Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/browser';
import { ContextLines } from '@sentry/integrations';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [new ContextLines()],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="inline-error-btn" onclick="throw new Error('Error with context lines')">Click me</button>
</body>
<footer>
Some text...
</foot>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers';

sentryTest(
'should add source context lines around stack frames from errors in Html inline JS',
async ({ getLocalTestPath, page, browserName }) => {
if (browserName === 'webkit') {
// The error we're throwing in this test is thrown as "Script error." in Webkit.
// We filter "Script error." out by default in `InboundFilters`.
// I don't think there's much value to disable InboundFilters defaults for this test,
// given that most of our users won't do that either.
// Let's skip it instead for Webkit.
sentryTest.skip();
}

const url = await getLocalTestPath({ testDir: __dirname });

const eventReqPromise = waitForErrorRequestOnUrl(page, url);

const clickPromise = page.click('#inline-error-btn');

const [req] = await Promise.all([eventReqPromise, clickPromise]);

const eventData = envelopeRequestParser(req);

expect(eventData.exception?.values).toHaveLength(1);

const exception = eventData.exception?.values?.[0];

expect(exception).toMatchObject({
stacktrace: {
frames: [
{
pre_context: ['<!DOCTYPE html>', '<html>', '<head>', ' <meta charset="utf-8">', ' </head>', ' <body>'],
context_line:
' <button id="inline-error-btn" onclick="throw new Error(\'Error with context lines\')">Click me</button>',
post_context: [
expect.stringContaining('<script'), // this line varies in the test based on tarball/cdn bundle (+variants)
' <footer>',
' Some text...',
' ',
'',
'</footer></body>',
'</html>',
],
},
],
},
});
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
document.getElementById('script-error-btn').addEventListener('click', () => {
throw new Error('Error without context lines');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="script-error-btn">Click me</button>
</body>
<footer>
Some text...
</foot>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers';

sentryTest('should not add source context lines to errors from script files', async ({ getLocalTestPath, page }) => {
const url = await getLocalTestPath({ testDir: __dirname });

const eventReqPromise = waitForErrorRequestOnUrl(page, url);

const clickPromise = page.click('#script-error-btn');

const [req] = await Promise.all([eventReqPromise, clickPromise]);

const eventData = envelopeRequestParser(req);

const exception = eventData.exception?.values?.[0];
const frames = exception?.stacktrace?.frames;
expect(frames).toHaveLength(1);
frames?.forEach(f => {
expect(f).not.toHaveProperty('pre_context');
expect(f).not.toHaveProperty('context_line');
expect(f).not.toHaveProperty('post_context');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script>
function throwTestError() {
throw new Error('Error with context lines')
}
</script>
</head>
<body>
<button id="inline-error-btn" onclick="throwTestError()">Click me</button>
</body>
<footer>
Some text...
</foot>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { expect } from '@playwright/test';

import { sentryTest } from '../../../../utils/fixtures';
import { envelopeRequestParser, waitForErrorRequestOnUrl } from '../../../../utils/helpers';

sentryTest(
'should add source context lines around stack frames from errors in Html script tags',
async ({ getLocalTestPath, page, browserName }) => {
if (browserName === 'webkit') {
// The error we're throwing in this test is thrown as "Script error." in Webkit.
// We filter "Script error." out by default in `InboundFilters`.
// I don't think there's much value to disable InboundFilters defaults for this test,
// given that most of our users won't do that either.
// Let's skip it instead for Webkit.
sentryTest.skip();
}

const url = await getLocalTestPath({ testDir: __dirname });

const eventReqPromise = waitForErrorRequestOnUrl(page, url);

const clickPromise = page.click('#inline-error-btn');

const [req] = await Promise.all([eventReqPromise, clickPromise]);

const eventData = envelopeRequestParser(req);

expect(eventData.exception?.values).toHaveLength(1);

const exception = eventData.exception?.values?.[0];

expect(exception).toMatchObject({
stacktrace: {
frames: [
{
lineno: 12,
pre_context: [
' <script>',
' function throwTestError() {',
" throw new Error('Error with context lines')",
' }',
' </script>',
' </head>',
' <body>',
],
context_line: ' <button id="inline-error-btn" onclick="throwTestError()">Click me</button>',
post_context: [
expect.stringContaining('<script'), // this line varies in the test based on tarball/cdn bundle (+variants)
' <footer>',
' Some text...',
' ',
'',
'</footer></body>',
'</html>',
],
},
{
lineno: 7,
pre_context: [
'<!DOCTYPE html>',
'<html>',
'<head>',
' <meta charset="utf-8">',
' <script>',
' function throwTestError() {',
],
context_line: " throw new Error('Error with context lines')",
post_context: [
' }',
' </script>',
' </head>',
' <body>',
' <button id="inline-error-btn" onclick="throwTestError()">Click me</button>',
expect.stringContaining('<script'), // this line varies in the test based on tarball/cdn bundle (+variants)
' <footer>',
],
},
],
},
});
},
);
111 changes: 111 additions & 0 deletions packages/integrations/src/contextlines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types';
import { addContextToFrame, GLOBAL_OBJ, stripUrlQueryAndFragment } from '@sentry/utils';

const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;

const DEFAULT_LINES_OF_CONTEXT = 7;

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 lines of stackframes pointing to JS embedded in
* the current page's HTML.
*
* This integration 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 ingestion and symbolication
* by attempting to download the JS files to the Sentry backend.
*
* Use this integration if you have inline JS code in HTML pages that can't be accessed
* by our backend (e.g. due to a login-protected page).
*/
export class ContextLines implements Integration {
/**
* @inheritDoc
*/
public static id: string = 'ContextLines';

/**
* @inheritDoc
*/
public name: string;

public constructor(private readonly _options: ContextLinesOptions = {}) {
this.name = ContextLines.id;
}

/**
* @inheritDoc
*/
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
addGlobalEventProcessor(event => {
const self = getCurrentHub().getIntegration(ContextLines);
if (!self) {
return event;
}
return this.addSourceContext(event);
});
}

/** Processes an event and adds context lines */
public addSourceContext(event: Event): Event {
const doc = WINDOW.document;
Copy link
Member

Choose a reason for hiding this comment

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

m: should only run if the current hub has the ContextLines integration.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm good point. I guess the Node ContextLines integration should have this check as well, right? Seems like we do it in some integrations and in others it's missing.

Copy link
Member

Choose a reason for hiding this comment

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

yeah we need to include the check there too

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;
if (!html) {
return event;
}

const htmlLines = ['<!DOCTYPE html>', '<html>', ...html.split('\n'), '</html>'];

exceptions.forEach(exception => {
const stacktrace = exception.stacktrace;
if (stacktrace && stacktrace.frames) {
stacktrace.frames = stacktrace.frames.map(frame =>
applySourceContextToFrame(
frame,
htmlLines,
htmlFilename,
this._options.frameContextLines != null ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT,
),
);
}
});

return event;
}
}

/**
* Only exported for testing
*/
export function applySourceContextToFrame(
frame: StackFrame,
htmlLines: string[],
htmlFilename: string,
linesOfContext: number,
): StackFrame {
if (frame.filename !== htmlFilename || !frame.lineno || !htmlLines.length) {
return frame;
}

addContextToFrame(htmlLines, frame, linesOfContext);

return frame;
}
1 change: 1 addition & 0 deletions packages/integrations/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { RewriteFrames } from './rewriteframes';
export { SessionTiming } from './sessiontiming';
export { Transaction } from './transaction';
export { HttpClient } from './httpclient';
export { ContextLines } from './contextlines';
Loading