-
Notifications
You must be signed in to change notification settings - Fork 214
Initial propagation headers #1457
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9d28078
8b42186
8b640b3
33666df
e3dd993
a6669ee
d202180
5161ca4
f65c5b7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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)); | ||
| if (!sessionId) { | ||
| return next(); | ||
| } | ||
| return storage.run({ sessionId }, () => next()); | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) { | ||
|
|
@@ -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}`; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIRC, this means we only forward the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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, | ||
|
|
||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.