Skip to content

Commit

Permalink
feat: Capture request, response headers and body (#9)
Browse files Browse the repository at this point in the history
* feat: Capture request, response headers and body

* remove try-catch

* max body size + try catch on stringify

* normalize header key and value according to otel

* increase max body size
  • Loading branch information
ernestii authored Jun 5, 2024
1 parent 4ba1a7b commit d549200
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/purple-monkeys-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hyperdx/otel-react-native': patch
---

feat: Capture request headers, request body and response body
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ HyperDXRum.init({
apiKey: '<YOUR_API_KEY_HERE>',
tracePropagationTargets: [/api.myapp.domain/i], // Set to link traces from frontend to backend requests
networkHeadersCapture: false,
networkBodyCapture: false,
});
```

Expand All @@ -39,7 +40,8 @@ HyperDXRum.init({
- `apiKey`: Your HyperDX Ingestion API key. You can find it [here](https://www.hyperdx.io/team).
- `service`: Name of your application. Set it to distinguish your app from others in HyperDX.
- `tracePropagationTargets`: A list of regular expressions that match the URLs of your backend services. Set it to link traces from frontend to backend requests.
- `networkHeadersCapture`: networkHeadersCapture is a flag that allows the option to capture header information, with the default flag being false.
- `networkHeadersCapture`: networkHeadersCapture is a flag that allows the option to capture request and response headers information, with the default flag being false.
- `networkBodyCapture`: networkBodyCapture is a flag that allows the option to capture request and response body information, with the default flag being false.

### (Optional) Attach User Information or Metadata

Expand Down
1 change: 0 additions & 1 deletion src/globalAttributeAppender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ export default class GlobalAttributeAppender implements SpanProcessor {

onStart(span: Span): void {
span.setAttributes(getGlobalAttributes());
span.setAttribute('_hyperdx_operation', span.name);
}
shutdown(): Promise<void> {
return Promise.resolve();
Expand Down
101 changes: 86 additions & 15 deletions src/instrumentations/xhr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ import type { PropagateTraceHeaderCorsUrls } from '@opentelemetry/sdk-trace-web/

const parseUrl = (url: string) => new URL(url);

const MAX_BODY_LENGTH = 2 * 1024 * 1024; // 2MB

interface XhrConfig {
clearTimingResources?: boolean;
ignoreUrls: Array<string | RegExp> | undefined;
propagateTraceHeaderCorsUrls?: (string | RegExp)[];
networkHeadersCapture?: boolean;
networkBodyCapture?: boolean;
}

class TaskCounter {
Expand Down Expand Up @@ -570,6 +573,7 @@ export function instrumentXHROriginal(config: XhrConfig) {
export function instrumentXHR(config: XhrConfig) {
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
const originalSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;

const tracer = api.trace.getTracer('xhr');
const taskCounter = new TaskCounter();
Expand Down Expand Up @@ -662,31 +666,47 @@ export function instrumentXHR(config: XhrConfig) {
}
}

function _normalizeHeaders(
type: string,
headersString: string
): { [key: string]: string } {
function _normalizeHeader([key, value]: [string, string]): Record<
string,
api.AttributeValue
> {
const normalizedKey = key.toLowerCase().replace(/-/g, '_').trim();
let normalizedValue: api.AttributeValue;

// https://github.com/open-telemetry/opentelemetry-js/blob/82b7526b028a34a23936016768f37df05effcd59/experimental/packages/opentelemetry-instrumentation-http/src/utils.ts#L604C1-L611C1
if (typeof value === 'string') {
normalizedValue = [value];
} else if (Array.isArray(value)) {
normalizedValue = value;
} else {
normalizedValue = [value];
}
return { [normalizedKey]: normalizedValue };
}

function _normalizeHeaders(headersString: string): {
[key: string]: api.AttributeValue;
} {
const lines = headersString.trim().split('\n');
const normalizedHeaders: { [key: string]: string } = {};
const normalizedHeaders: { [key: string]: api.AttributeValue } = {};

lines.forEach((line) => {
let [key, value] = line.split(/:\s*/);
let [key, value] = line.trim().split(/:\s*/);
if (key && value) {
key = key.replace(/-/g, '_').toLowerCase();
const newKey = `http.${type}.header.${key}`;
normalizedHeaders[newKey] = value.trim();
Object.assign(normalizedHeaders, _normalizeHeader([key, value]));
}
});

return normalizedHeaders;
}

function _setHeaderAttributeForSpan(
normalizedHeader: { [key: string]: string },
normalizedHeaders: { [key: string]: api.AttributeValue },
type: 'request' | 'response',
span: api.Span
) {
Object.entries(normalizedHeader).forEach(([key, value]) => {
span.setAttribute(key, value as api.AttributeValue);
Object.entries(normalizedHeaders).forEach(([key, value]) => {
span.setAttribute(`http.${type}.header.${key}`, value);
});
}

Expand Down Expand Up @@ -722,12 +742,30 @@ export function instrumentXHR(config: XhrConfig) {

function _handleHeaderCapture(headers: string, currentSpan: api.Span) {
if (config.networkHeadersCapture) {
const normalizedHeaders = _normalizeHeaders('response', headers);
_setHeaderAttributeForSpan(normalizedHeaders, currentSpan);
const normalizedHeaders = _normalizeHeaders(headers);
_setHeaderAttributeForSpan(normalizedHeaders, 'response', currentSpan);
}
}

if (config.networkHeadersCapture) {
XMLHttpRequest.prototype.setRequestHeader = function (
this: XMLHttpRequest,
...args
) {
const [key, value] = args;
const xhrMem = _xhrMem.get(this);
if (xhrMem && key && value) {
const normalizedHeader = _normalizeHeader([key, value]);
// TODO: Store and dedupe the headers before adding them to the span
_setHeaderAttributeForSpan(normalizedHeader, 'request', xhrMem.span);
}
originalSetRequestHeader.apply(this, args);
};
}

XMLHttpRequest.prototype.send = function (this: XMLHttpRequest, ...args) {
const requestBody = args[0];

const xhrMem = _xhrMem.get(this);
if (!xhrMem) {
return originalSend.apply(this, args);
Expand All @@ -742,6 +780,22 @@ export function instrumentXHR(config: XhrConfig) {
taskCounter.increment();
xhrMem.sendStartTime = hrTime();
currentSpan.addEvent(EventNames.METHOD_SEND);
if (config.networkBodyCapture) {
let body: string = '';
if (typeof requestBody === 'string') {
body = requestBody;
} else {
try {
body = JSON.stringify(requestBody);
} catch (e) {
body = '[object of type ' + typeof requestBody + ']';
}
}
currentSpan.setAttribute(
'http.request.body',
body.slice(0, MAX_BODY_LENGTH)
);
}
this.addEventListener('readystatechange', () => {
if (this.readyState === XMLHttpRequest.HEADERS_RECEIVED) {
const headers = this.getAllResponseHeaders().toLowerCase();
Expand All @@ -753,7 +807,24 @@ export function instrumentXHR(config: XhrConfig) {
}
}
} else if (this.readyState === XMLHttpRequest.DONE) {
endSpan(EventNames.EVENT_READY_STATE_CHANGE, this);
if (config.networkBodyCapture && this.responseType === 'blob') {
new Response(this.response)
.text()
.then((text) => {
currentSpan.setAttribute('http.response.body', text);
})
.finally(() => {
endSpan(EventNames.EVENT_READY_STATE_CHANGE, this);
});
} else {
if (config.networkBodyCapture) {
currentSpan.setAttribute(
'http.response.body',
this.responseText
);
}
endSpan(EventNames.EVENT_READY_STATE_CHANGE, this);
}
}
});
addHeaders(this, spanUrl);
Expand Down
2 changes: 2 additions & 0 deletions src/splunkRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface ReactNativeConfiguration {
beaconEndpoint?: string;
apiKey: string;
networkHeadersCapture?: boolean;
networkBodyCapture?: boolean;
service: string;
deploymentEnvironment?: string;
allowInsecureBeacon?: boolean;
Expand Down Expand Up @@ -171,6 +172,7 @@ export const HyperDXRum: HyperDXRumType = {
ignoreUrls: config.ignoreUrls,
propagateTraceHeaderCorsUrls: config.tracePropagationTargets,
networkHeadersCapture: config.networkHeadersCapture,
networkBodyCapture: config.networkBodyCapture,
});
instrumentErrors();

Expand Down

0 comments on commit d549200

Please sign in to comment.