From bd146dd2ca5e73dd792889726301cbc8d527ab3d Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 28 Jul 2023 13:14:56 +0200 Subject: [PATCH 01/13] add integration boilerplate --- .../browser/src/integrations/contextlines.ts | 49 +++++++++++++++++++ packages/browser/src/integrations/index.ts | 1 + 2 files changed, 50 insertions(+) create mode 100644 packages/browser/src/integrations/contextlines.ts diff --git a/packages/browser/src/integrations/contextlines.ts b/packages/browser/src/integrations/contextlines.ts new file mode 100644 index 000000000000..23b4eb152b6a --- /dev/null +++ b/packages/browser/src/integrations/contextlines.ts @@ -0,0 +1,49 @@ +import type { Event, EventProcessor, Integration } from '@sentry/types'; + +interface ContextLinesOptions { + /** + * Sets the number of context lines for each frame when loading a file. + * Defaults to 7. + * + * Set to 0 to disable loading and inclusion of source files. + **/ + frameContextLines?: number; +} + +/** + * Collects source context lines around the line of a stackframe + * This integration only works for stack frames pointing to JS that's directly embedded + * in HTML files. + * It DOES NOT work for stack frames pointing to JS files that are loaded by the browser. + * For frames pointing to files, context lines are added during ingestino and symbolication + * by attempting to download the JS files to the Sentry backend. + */ +export class ContextLines implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'ContextLines'; + + /** + * @inheritDoc + */ + public name: string = ContextLines.id; + + public constructor( + private readonly _options: ContextLinesOptions = { + frameContextLines: 0, + }, + ) {} + + /** + * @inheritDoc + */ + public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void): void { + addGlobalEventProcessor(event => this.addSourceContext(event)); + } + + /** Processes an event and adds context lines */ + public addSourceContext(event: Event): Event { + return event; + } +} diff --git a/packages/browser/src/integrations/index.ts b/packages/browser/src/integrations/index.ts index e029422f363c..ec826b561000 100644 --- a/packages/browser/src/integrations/index.ts +++ b/packages/browser/src/integrations/index.ts @@ -4,3 +4,4 @@ export { Breadcrumbs } from './breadcrumbs'; export { LinkedErrors } from './linkederrors'; export { HttpContext } from './httpcontext'; export { Dedupe } from './dedupe'; +export { ContextLines } from './contextlines'; From a8685deb8d2971f48bec842d460ca5b517901f87 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 28 Jul 2023 18:40:15 +0200 Subject: [PATCH 02/13] feat(browser): Add `ContextLines` integration for html-embedded JS stack frames --- .../browser/src/integrations/contextlines.ts | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/packages/browser/src/integrations/contextlines.ts b/packages/browser/src/integrations/contextlines.ts index 23b4eb152b6a..91bd817acac9 100644 --- a/packages/browser/src/integrations/contextlines.ts +++ b/packages/browser/src/integrations/contextlines.ts @@ -1,4 +1,7 @@ -import type { Event, EventProcessor, Integration } from '@sentry/types'; +import type { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; +import { stripUrlQueryAndFragment } from '@sentry/utils'; + +import { WINDOW } from '../helpers'; interface ContextLinesOptions { /** @@ -29,11 +32,7 @@ export class ContextLines implements Integration { */ public name: string = ContextLines.id; - public constructor( - private readonly _options: ContextLinesOptions = { - frameContextLines: 0, - }, - ) {} + public constructor(private readonly _options: ContextLinesOptions = {}) {} /** * @inheritDoc @@ -44,6 +43,70 @@ export class ContextLines implements Integration { /** Processes an event and adds context lines */ public addSourceContext(event: Event): Event { + const doc = WINDOW.document; + const htmlFilename = WINDOW.location && stripUrlQueryAndFragment(WINDOW.location.href); + if (!doc || !htmlFilename) { + return event; + } + + const exceptions = event.exception && event.exception.values; + if (!exceptions || !exceptions.length) { + return event; + } + + const html = doc.documentElement.innerHTML; + + const htmlLines = ['', '', ...html.split('\n'), '']; + if (!htmlLines.length) { + return event; + } + + exceptions.forEach(exception => { + const stacktrace = exception.stacktrace; + if (stacktrace && stacktrace.frames) { + stacktrace.frames = stacktrace.frames.map(frame => + applySourceContextToFrame(frame, htmlLines, htmlFilename, this._options.frameContextLines || 7), + ); + } + }); + return event; } } + +/** + * Only exported for testing + */ +export function applySourceContextToFrame( + frame: StackFrame, + htmlLines: string[], + htmlFilename: string, + contextRange: number, +): StackFrame { + if (frame.filename !== htmlFilename || !frame.lineno || !htmlLines.length) { + return frame; + } + + const sourroundingRange = Math.floor(contextRange / 2); + const contextLineIndex = frame.lineno - 1; + const preStartIndex = Math.max(contextLineIndex - sourroundingRange, 0); + const postEndIndex = Math.min(contextLineIndex + sourroundingRange, htmlLines.length - 1); + + const preLines = htmlLines.slice(preStartIndex, contextLineIndex); + const contextLine = htmlLines[contextLineIndex]; + const postLines = htmlLines.slice(contextLineIndex + 1, postEndIndex + 1); + + if (preLines.length) { + frame.pre_context = preLines; + } + + if (contextLine) { + frame.context_line = contextLine; + } + + if (postLines.length) { + frame.post_context = postLines || undefined; + } + + return frame; +} From 0e15ba0546ab5e9a3eb98c65113750a548861ef5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 28 Jul 2023 18:41:10 +0200 Subject: [PATCH 03/13] add tests --- .../suites/integrations/ContextLines/init.js | 8 ++ .../integrations/ContextLines/subject.js | 3 + .../integrations/ContextLines/template.html | 13 ++ .../suites/integrations/ContextLines/test.ts | 59 +++++++++ .../unit/integrations/contextlines.test.ts | 115 ++++++++++++++++++ 5 files changed, 198 insertions(+) create mode 100644 packages/browser-integration-tests/suites/integrations/ContextLines/init.js create mode 100644 packages/browser-integration-tests/suites/integrations/ContextLines/subject.js create mode 100644 packages/browser-integration-tests/suites/integrations/ContextLines/template.html create mode 100644 packages/browser-integration-tests/suites/integrations/ContextLines/test.ts create mode 100644 packages/browser/test/unit/integrations/contextlines.test.ts diff --git a/packages/browser-integration-tests/suites/integrations/ContextLines/init.js b/packages/browser-integration-tests/suites/integrations/ContextLines/init.js new file mode 100644 index 000000000000..93d8362fbac0 --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/ContextLines/init.js @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Sentry.Integrations.ContextLines()], +}); diff --git a/packages/browser-integration-tests/suites/integrations/ContextLines/subject.js b/packages/browser-integration-tests/suites/integrations/ContextLines/subject.js new file mode 100644 index 000000000000..744649fb291c --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/ContextLines/subject.js @@ -0,0 +1,3 @@ +document.getElementById('script-error-btn').addEventListener('click', () => { + throw new Error('Error without context lines'); +}); diff --git a/packages/browser-integration-tests/suites/integrations/ContextLines/template.html b/packages/browser-integration-tests/suites/integrations/ContextLines/template.html new file mode 100644 index 000000000000..790dddf90c81 --- /dev/null +++ b/packages/browser-integration-tests/suites/integrations/ContextLines/template.html @@ -0,0 +1,13 @@ + + + + + + + + + +