From 43ac2d7467136d33f4f4225e919babe05dfe555d Mon Sep 17 00:00:00 2001 From: jairo <75110848+jairo-mendoza@users.noreply.github.com> Date: Wed, 13 Nov 2024 10:18:32 -0800 Subject: [PATCH] feat: Add additional fields collected by the TraceKit package for stack traces (#383) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Short description of the changes Introduces the [TraceKit](https://github.com/csnover/TraceKit) package to the web distro. Adds additional fields with the help of the `TraceKit` package. These additional fields are prefixed with `exception.structured_stacktrace`. Additional fields will be sent by `GlobalErrorsInstrumentation`. ## How to verify that this has the expected result I tested locally using the `hello-world-web` example provided by the web-distro. Made the example to throw a simple error. This was the result I received on Honeycomb: Screenshot 2024-11-07 at 3 34 27 PM Big thanks to @pkanal for helping with this change! --- .../package-lock.json | 7 +++++ .../honeycomb-opentelemetry-web/package.json | 1 + .../src/global-errors-autoinstrumentation.ts | 30 +++++++++++++++++++ .../global-errors-instrumentation.test.ts | 21 +++++++++++++ 4 files changed, 59 insertions(+) diff --git a/packages/honeycomb-opentelemetry-web/package-lock.json b/packages/honeycomb-opentelemetry-web/package-lock.json index b99989cb..d39f33b1 100644 --- a/packages/honeycomb-opentelemetry-web/package-lock.json +++ b/packages/honeycomb-opentelemetry-web/package-lock.json @@ -21,6 +21,7 @@ "@opentelemetry/sdk-trace-web": "~1.27.0", "@opentelemetry/semantic-conventions": "~1.27.0", "shimmer": "^1.2.1", + "tracekit": "^0.4.7", "ua-parser-js": "^1.0.37", "web-vitals": "^4.2.3" }, @@ -14255,6 +14256,12 @@ "node": ">=12" } }, + "node_modules/tracekit": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/tracekit/-/tracekit-0.4.7.tgz", + "integrity": "sha512-d3BLRambfZ0VOYLIh7FRSElugsOMrNgxzT4jXyOXC7lnhoeEJIHNg8VAcpKVVmu9kREGUAVg8Eh3SlRfkU/ksQ==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", diff --git a/packages/honeycomb-opentelemetry-web/package.json b/packages/honeycomb-opentelemetry-web/package.json index 5922a6d3..2e20bfad 100644 --- a/packages/honeycomb-opentelemetry-web/package.json +++ b/packages/honeycomb-opentelemetry-web/package.json @@ -86,6 +86,7 @@ "@opentelemetry/sdk-trace-web": "~1.27.0", "@opentelemetry/semantic-conventions": "~1.27.0", "shimmer": "^1.2.1", + "tracekit": "^0.4.7", "ua-parser-js": "^1.0.37", "web-vitals": "^4.2.3" } diff --git a/packages/honeycomb-opentelemetry-web/src/global-errors-autoinstrumentation.ts b/packages/honeycomb-opentelemetry-web/src/global-errors-autoinstrumentation.ts index 00e27532..01e40a6e 100644 --- a/packages/honeycomb-opentelemetry-web/src/global-errors-autoinstrumentation.ts +++ b/packages/honeycomb-opentelemetry-web/src/global-errors-autoinstrumentation.ts @@ -7,6 +7,7 @@ import { SEMATTRS_EXCEPTION_STACKTRACE, SEMATTRS_EXCEPTION_TYPE, } from '@opentelemetry/semantic-conventions'; +import { computeStackTrace, StackFrame } from 'tracekit'; export interface GlobalErrorsInstrumentationConfig extends InstrumentationConfig {} @@ -26,6 +27,34 @@ export class GlobalErrorsInstrumentation extends InstrumentationAbstract { this._isEnabled = enabled; } + _computeStackTrace = (error: Error | undefined) => { + if (!error) { + return {}; + } + + // OTLP does not accept arrays of objects + // breaking down the stack into arrays of strings/numbers + const structuredStack: StackFrame[] = computeStackTrace(error).stack; + const lines: number[] = []; + const columns: number[] = []; + const functions: string[] = []; + const urls: string[] = []; + + for (const stackFrame of structuredStack) { + lines.push(stackFrame.line); + columns.push(stackFrame.column); + functions.push(stackFrame.func); + urls.push(stackFrame.url); + } + + return { + 'exception.structured_stacktrace.columns': columns, + 'exception.structured_stacktrace.lines': lines, + 'exception.structured_stacktrace.functions': functions, + 'exception.structured_stacktrace.urls': urls, + }; + }; + onError = (event: ErrorEvent | PromiseRejectionEvent) => { const error: Error | undefined = 'reason' in event ? event.reason : event.error; @@ -35,6 +64,7 @@ export class GlobalErrorsInstrumentation extends InstrumentationAbstract { [SEMATTRS_EXCEPTION_TYPE]: type, [SEMATTRS_EXCEPTION_MESSAGE]: message, [SEMATTRS_EXCEPTION_STACKTRACE]: error?.stack, + ...this._computeStackTrace(error), }; // otel spec requires at minimum these two if (!message || !type) return; diff --git a/packages/honeycomb-opentelemetry-web/test/global-errors-instrumentation.test.ts b/packages/honeycomb-opentelemetry-web/test/global-errors-instrumentation.test.ts index 7e98f854..c113fc25 100644 --- a/packages/honeycomb-opentelemetry-web/test/global-errors-instrumentation.test.ts +++ b/packages/honeycomb-opentelemetry-web/test/global-errors-instrumentation.test.ts @@ -33,10 +33,15 @@ describe('Global Errors Instrumentation Tests', () => { const span = exporter.getFinishedSpans()[0]; expect(span.name).toBe('exception'); + // TODO: Mock a stack trace and test that it returns the correct keys and values expect(span.attributes).toMatchObject({ 'exception.type': 'Error', 'exception.message': 'Something happened', 'exception.stacktrace': expect.any(String), + 'exception.structured_stacktrace.columns': expect.any(Array), + 'exception.structured_stacktrace.lines': expect.any(Array), + 'exception.structured_stacktrace.functions': expect.any(Array), + 'exception.structured_stacktrace.urls': expect.any(Array), }); }); @@ -60,4 +65,20 @@ describe('Global Errors Instrumentation Tests', () => { }); }); }); + + describe('_computeStackTrace', () => { + it('should return an empty object if error is undefined', () => { + expect(instr._computeStackTrace(undefined)).toEqual({}); + }); + + // TODO: Mock a stack trace and test that it returns the correct keys and values + it('should return an object with structured stack trace information', () => { + expect(instr._computeStackTrace(new Error('This is an error'))).toEqual({ + 'exception.structured_stacktrace.columns': expect.any(Array), + 'exception.structured_stacktrace.lines': expect.any(Array), + 'exception.structured_stacktrace.functions': expect.any(Array), + 'exception.structured_stacktrace.urls': expect.any(Array), + }); + }); + }); });