Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ declare class Rollbar implements Rollbar.Components {
): Rollbar.LambdaHandler<T>;

public errorHandler(): Rollbar.ExpressErrorHandler;
public expressMiddleware(): Rollbar.ExpressMiddleware | undefined;

// Components

Expand Down Expand Up @@ -219,6 +220,11 @@ declare namespace Rollbar {
response: any,
next: ExpressNextFunction,
) => any;
export type ExpressMiddleware = (
request: any,
response: any,
next: ExpressNextFunction,
) => any;
export type ExpressNextFunction = (err?: any) => void;
class Locals {}
export type LocalsType = typeof Locals;
Expand Down Expand Up @@ -333,9 +339,18 @@ declare namespace Rollbar {
export interface TransformSpanParams {
span: any;
}
export type TracingPropagationHeader =
| 'baggage'
| 'traceparent'
| 'tracestate';
export interface TracingPropagationOptions {
enabledHeaders?: TracingPropagationHeader[];
enabledCorsUrls?: (string | RegExp)[];
}
export interface TracingOptions {
enabled?: boolean;
endpoint?: string;
propagation?: TracingPropagationOptions;
transformSpan?: (params: TransformSpanParams) => void;
}

Expand Down
13 changes: 12 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"ts-node": "^10.9.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.46.4",
"undici": "^6.23.0",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-node-externals": "^3.0.0"
Expand Down
25 changes: 25 additions & 0 deletions src/browser/telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,23 @@ class Instrumenter {
function (orig) {
return function (data) {
const xhr = this;
const tracing = self.rollbar?.tracing;
if (
_.shouldAddBaggageHeader(
self.options,
tracing,
xhr.__rollbar_xhr?.url,
)
) {
try {
xhr.setRequestHeader(
'baggage',
`rollbar.session.id=${tracing.sessionId}`,
);
} catch (_e) {
/* ignore errors from adding baggage header */
}
}

function onreadystatechangeHandler() {
if (xhr.__rollbar_xhr) {
Expand Down Expand Up @@ -435,6 +452,14 @@ class Instrumenter {
if (args[1] && args[1].method) {
method = args[1].method;
}
const tracing = self.rollbar?.tracing;
if (_.shouldAddBaggageHeader(self.options, tracing, url)) {
const headers = {
baggage: `rollbar.session.id=${tracing.sessionId}`,
};

_.addHeadersToFetch(args, headers);
}
const metadata = {
method: method,
url: url,
Expand Down
4 changes: 3 additions & 1 deletion src/rollbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,11 @@ Rollbar.prototype._log = function (defaultLevel, item) {

Rollbar.prototype._addItemAttributes = function (item) {
const span = this.tracing?.getSpan();
const asyncLocalSessionId = _.getSessionIdFromAsyncLocalStorage(this);
const sessionId = asyncLocalSessionId || this.tracing?.sessionId;

const attributes = [
{ key: 'session_id', value: this.tracing?.sessionId },
{ key: 'session_id', value: sessionId },
{ key: 'span_id', value: span?.spanId },
{ key: 'trace_id', value: span?.traceId },
];
Expand Down
63 changes: 63 additions & 0 deletions src/server/middleware/rollbarExpressMiddleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { AsyncLocalStorage } from 'node:async_hooks';

export function extractSessionId(headerValue) {
if (!headerValue) {
return null;
}
const rawValue = Array.isArray(headerValue)
? headerValue.join(',')
: headerValue;
if (typeof rawValue !== 'string') {
return null;
}
const entries = rawValue.split(',');
for (const entry of entries) {
const trimmed = entry.trim();
if (!trimmed) {
continue;
}
const equalsIndex = trimmed.indexOf('=');
if (equalsIndex === -1) {
continue;
}
const key = trimmed.slice(0, equalsIndex).trim();
if (key !== 'rollbar.session.id') {
continue;
}
const value = trimmed.slice(equalsIndex + 1).trim();
if (!value) {
return null;
}
try {
return decodeURIComponent(value);
} catch (_e) {
return value;
}
}
return null;
}

function getBaggageHeader(req) {
if (!req) {
return null;
}
if (typeof req.get === 'function') {
return req.get('baggage');
}
return req.headers?.baggage || null;
}

export default function rollbarExpressMiddleware(rollbar) {
const storage = rollbar?.client.asyncLocalStorage || new AsyncLocalStorage();
if (rollbar) {
rollbar.client.asyncLocalStorage = storage;
}

return function rollbarExpressMiddlewareHandler(req, _res, next) {
const sessionId = extractSessionId(getBaggageHeader(req));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to create a sessionId, if one is not present?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a doc forthcoming to discuss that. Whatever we do, it won't go in this PR.

if (!sessionId) {
return next();
}
return storage.run({ sessionId }, () => next());
};
}
13 changes: 13 additions & 0 deletions src/server/rollbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import truncation from '../truncation.js';
import * as _ from '../utility.js';

import * as serverDefaults from './defaults.js';
import rollbarExpressMiddleware from './middleware/rollbarExpressMiddleware.js';
import Instrumenter from './telemetry.js';
import * as transforms from './transforms.js';
import Transport from './transport.js';
Expand Down Expand Up @@ -460,6 +461,18 @@ Rollbar.wrapCallback = function (f) {
}
};

Rollbar.prototype.expressMiddleware = function () {
return rollbarExpressMiddleware(this);
};

Rollbar.expressMiddleware = function () {
if (_instance) {
return rollbarExpressMiddleware(_instance);
}
handleUninitialized();
return undefined;
};

Rollbar.prototype.captureEvent = function () {
var event = _.createTelemetryEvent(arguments);
return this.client.captureEvent(event.type, event.metadata, event.level);
Expand Down
121 changes: 120 additions & 1 deletion src/server/telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@ Instrumenter.prototype.instrumentNetwork = function () {
this.replacements,
'network',
);
if (typeof globalThis.fetch === 'function') {
replace(
globalThis,
'fetch',
fetchRequestWrapper.bind(this),
this.replacements,
'network',
);
}
};

function networkRequestWrapper(orig) {
Expand All @@ -93,10 +102,22 @@ function networkRequestWrapper(orig) {
return (...args) => {
const [url, options, cb] = args;
var mergedOptions = urlHelpers.mergeOptions(url, options, cb);
const requestUrl = urlHelpers.constructUrl(mergedOptions.options);
const sessionId = _.getSessionIdFromAsyncLocalStorage(this.rollbar.client);

if (
sessionId &&
_.shouldAddBaggageHeader(this.options, { sessionId }, requestUrl)
) {
if (!mergedOptions.options.headers) {
mergedOptions.options.headers = {};
}
mergedOptions.options.headers.baggage = `rollbar.session.id=${sessionId}`;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC, this means we only forward the rollbar.session.id in the baggage header. Is there a reason why wouldn't we forward the entire thing?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Answered async - duplicates of the same header are concatenated

}

var metadata = {
method: mergedOptions.options.method || 'GET',
url: urlHelpers.constructUrl(mergedOptions.options),
url: requestUrl,
status_code: null,
start_time_ms: _.now(),
end_time_ms: null,
Expand Down Expand Up @@ -142,6 +163,104 @@ function responseCallbackWrapper(options, metadata, callback) {
};
}

function fetchRequestWrapper(orig) {
var telemeter = this.telemeter;

return (...args) => {
const input = args[0];
const init = args[1];
let method = 'GET';
let url;
const sessionId = _.getSessionIdFromAsyncLocalStorage(this.rollbar.client);

if (_.isType(input, 'string') || input instanceof URL) {
url = input.toString();
} else if (input) {
url = input.url;
if (input.method) {
method = input.method;
}
}

if (init && init.method) {
method = init.method;
}

if (
sessionId &&
_.shouldAddBaggageHeader(this.options, { sessionId }, url)
) {
const headers = { baggage: `rollbar.session.id=${sessionId}` };

_.addHeadersToFetch(args, headers);
}

const metadata = {
method: method,
url: url,
status_code: null,
start_time_ms: _.now(),
end_time_ms: null,
};

if (this.autoInstrument.networkRequestHeaders) {
const requestHeaders = normalizeFetchHeaders(
init && init.headers ? init.headers : input && input.headers,
);
if (requestHeaders) {
metadata.request_headers = requestHeaders;
}
}

telemeter.captureNetwork(metadata, 'fetch');

return orig.apply(globalThis, args).then(
(res) => {
metadata.end_time_ms = _.now();
metadata.status_code = res.status;
if (this.autoInstrument.networkResponseHeaders) {
const responseHeaders = normalizeFetchHeaders(res.headers);
if (responseHeaders) {
metadata.response = metadata.response || {};
metadata.response.headers = responseHeaders;
}
}
return res;
},
(err) => {
metadata.end_time_ms = _.now();
metadata.status_code = 0;
metadata.error = [err.name, err.message].join(': ');
throw err;
},
);
};
}

function normalizeFetchHeaders(headers) {
if (!headers) return null;
if (typeof headers.forEach === 'function') {
const normalized = {};
headers.forEach((value, key) => {
normalized[key] = value;
});
return normalized;
}
if (Array.isArray(headers)) {
const normalized = {};
headers.forEach((pair) => {
if (pair && pair.length > 1) {
normalized[pair[0]] = pair[1];
}
});
return normalized;
}
if (_.isType(headers, 'object')) {
return headers;
}
return null;
}

Instrumenter.prototype.captureNetwork = function (
metadata,
subtype,
Expand Down
Loading
Loading