Skip to content

Commit 74db527

Browse files
authored
chore(svelte): Detect and report SvelteKit usage (#5594)
Add a SvelteKit detection mechanism to our SDK and inserts a SvelteKit entry into `event.modules` if we detect that the frontend in which the SDK is used, was rendered by SvelteKit. To check this, we check for the existence of a div with the id `svelte-announcer` that's automatically injected into the rendered HTML by SvelteKit. It is used to improve accessibility (see sveltejs/kit#307) but we can leverage it as a SvelteKit detection criterion. Btw, we're not the first ones who came up with this approach, as it turns out: https://twitter.com/jhewt/status/1359632380483493889 Introduce a new utils function, `getDomElement` in which we consolidate the usage of `document.querySelector` in the SDK. So in addition to using this new function for obtaining the div described above, we now also call it in `BrowserTracing` to get `<meta>` tags. Add some tests to test the new behaviour and the helper functions. We might want to consider writing an integration test for this feature but this first requires a Svelte SDK integration test infrastructure. ref: #5573
1 parent 8dd6743 commit 74db527

File tree

5 files changed

+136
-15
lines changed

5 files changed

+136
-15
lines changed

packages/svelte/src/sdk.ts

+44-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browser';
2-
1+
import { addGlobalEventProcessor, BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browser';
2+
import type { EventProcessor } from '@sentry/types';
3+
import { getDomElement } from '@sentry/utils';
34
/**
45
* Inits the Svelte SDK
56
*/
@@ -17,4 +18,45 @@ export function init(options: BrowserOptions): void {
1718
};
1819

1920
browserInit(options);
21+
22+
detectAndReportSvelteKit();
23+
}
24+
25+
/**
26+
* Adds a global event processor to detect if the SDK is initialized in a SvelteKit frontend,
27+
* in which case we add SvelteKit an event.modules entry to outgoing events.
28+
* SvelteKit detection is performed only once, when the event processor is called for the
29+
* first time. We cannot perform this check upfront (directly when init is called) because
30+
* at this time, the HTML element might not yet be accessible.
31+
*/
32+
export function detectAndReportSvelteKit(): void {
33+
let detectedSvelteKit: boolean | undefined = undefined;
34+
35+
const svelteKitProcessor: EventProcessor = event => {
36+
if (detectedSvelteKit === undefined) {
37+
detectedSvelteKit = isSvelteKitApp();
38+
}
39+
if (detectedSvelteKit) {
40+
event.modules = {
41+
svelteKit: 'latest',
42+
...event.modules,
43+
};
44+
}
45+
return event;
46+
};
47+
svelteKitProcessor.id = 'svelteKitProcessor';
48+
49+
addGlobalEventProcessor(svelteKitProcessor);
50+
}
51+
52+
/**
53+
* To actually detect a SvelteKit frontend, we search the DOM for a special
54+
* div that's inserted by SvelteKit when the page is rendered. It's identifyed
55+
* by its id, 'svelte-announcer', and it's used to improve page accessibility.
56+
* This div is not present when only using Svelte without SvelteKit.
57+
*
58+
* @see https://github.com/sveltejs/kit/issues/307 for more information
59+
*/
60+
export function isSvelteKitApp(): boolean {
61+
return getDomElement('div#svelte-announcer') !== null;
2062
}

packages/svelte/test/sdk.test.ts

+60-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
1-
import { init as browserInitRaw, SDK_VERSION } from '@sentry/browser';
1+
import { addGlobalEventProcessor, init as browserInitRaw, SDK_VERSION } from '@sentry/browser';
2+
import { EventProcessor } from '@sentry/types';
23

3-
import { init as svelteInit } from '../src/sdk';
4+
import { detectAndReportSvelteKit, init as svelteInit, isSvelteKitApp } from '../src/sdk';
45

56
const browserInit = browserInitRaw as jest.Mock;
7+
const addGlobalEventProcessorFunction = addGlobalEventProcessor as jest.Mock;
8+
let passedEventProcessor: EventProcessor | undefined;
9+
addGlobalEventProcessorFunction.mockImplementation(proc => {
10+
passedEventProcessor = proc;
11+
});
12+
613
jest.mock('@sentry/browser');
714

815
describe('Initialize Svelte SDk', () => {
@@ -29,3 +36,54 @@ describe('Initialize Svelte SDk', () => {
2936
expect(browserInit).toHaveBeenCalledWith(expect.objectContaining(expectedMetadata));
3037
});
3138
});
39+
40+
describe('detectAndReportSvelteKit()', () => {
41+
const originalHtmlBody = document.body.innerHTML;
42+
afterEach(() => {
43+
jest.clearAllMocks();
44+
document.body.innerHTML = originalHtmlBody;
45+
passedEventProcessor = undefined;
46+
});
47+
48+
it('registers a global event processor', async () => {
49+
detectAndReportSvelteKit();
50+
51+
expect(addGlobalEventProcessorFunction).toHaveBeenCalledTimes(1);
52+
expect(passedEventProcessor?.id).toEqual('svelteKitProcessor');
53+
});
54+
55+
it('adds "SvelteKit" as a module to the event, if SvelteKit was detected', () => {
56+
document.body.innerHTML += '<div id="svelte-announcer">Home</div>';
57+
detectAndReportSvelteKit();
58+
59+
const processedEvent = passedEventProcessor && passedEventProcessor({} as unknown as any, {});
60+
61+
expect(processedEvent).toBeDefined();
62+
expect(processedEvent).toEqual({ modules: { svelteKit: 'latest' } });
63+
});
64+
65+
it("doesn't add anything to the event, if SvelteKit was not detected", () => {
66+
document.body.innerHTML = '';
67+
detectAndReportSvelteKit();
68+
69+
const processedEvent = passedEventProcessor && passedEventProcessor({} as unknown as any, {});
70+
71+
expect(processedEvent).toBeDefined();
72+
expect(processedEvent).toEqual({});
73+
});
74+
75+
describe('isSvelteKitApp()', () => {
76+
it('returns true if the svelte-announcer div is present', () => {
77+
document.body.innerHTML += '<div id="svelte-announcer">Home</div>';
78+
expect(isSvelteKitApp()).toBe(true);
79+
});
80+
it('returns false if the svelte-announcer div is not present (but similar elements)', () => {
81+
document.body.innerHTML += '<div id="svelte-something">Home</div>';
82+
expect(isSvelteKitApp()).toBe(false);
83+
});
84+
it('returns false if no div is present', () => {
85+
document.body.innerHTML = '';
86+
expect(isSvelteKitApp()).toBe(false);
87+
});
88+
});
89+
});

packages/tracing/src/browser/browsertracing.ts

+3-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable max-lines */
22
import { Hub } from '@sentry/hub';
33
import { EventProcessor, Integration, Transaction, TransactionContext } from '@sentry/types';
4-
import { getGlobalObject, logger, parseBaggageSetMutability } from '@sentry/utils';
4+
import { getDomElement, getGlobalObject, logger, parseBaggageSetMutability } from '@sentry/utils';
55

66
import { startIdleTransaction } from '../hubextensions';
77
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
@@ -294,13 +294,6 @@ export function extractTraceDataFromMetaTags(): Partial<TransactionContext> | un
294294

295295
/** Returns the value of a meta tag */
296296
export function getMetaContent(metaName: string): string | null {
297-
const globalObject = getGlobalObject<Window>();
298-
299-
// DOM/querySelector is not available in all environments
300-
if (globalObject.document && globalObject.document.querySelector) {
301-
const el = globalObject.document.querySelector(`meta[name=${metaName}]`);
302-
return el ? el.getAttribute('content') : null;
303-
} else {
304-
return null;
305-
}
297+
const metaTag = getDomElement(`meta[name=${metaName}]`);
298+
return metaTag ? metaTag.getAttribute('content') : null;
306299
}

packages/utils/src/browser.ts

+18
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,21 @@ export function getLocationHref(): string {
122122
return '';
123123
}
124124
}
125+
126+
/**
127+
* Gets a DOM element by using document.querySelector.
128+
*
129+
* This wrapper will first check for the existance of the function before
130+
* actually calling it so that we don't have to take care of this check,
131+
* every time we want to access the DOM.
132+
* Reason: DOM/querySelector is not available in all environments
133+
*
134+
* @param selector the selector string passed on to document.querySelector
135+
*/
136+
export function getDomElement(selector: string): Element | null {
137+
const global = getGlobalObject<Window>();
138+
if (global.document && global.document.querySelector) {
139+
return global.document.querySelector(selector);
140+
}
141+
return null;
142+
}

packages/utils/test/browser.test.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { JSDOM } from 'jsdom';
22

3-
import { htmlTreeAsString } from '../src/browser';
3+
import { getDomElement, htmlTreeAsString } from '../src/browser';
44

55
beforeAll(() => {
66
const dom = new JSDOM();
@@ -45,3 +45,13 @@ describe('htmlTreeAsString', () => {
4545
);
4646
});
4747
});
48+
49+
describe('getDomElement', () => {
50+
it('returns the element for a given query selector', () => {
51+
document.head.innerHTML = '<div id="mydiv">Hello</div>';
52+
const el = getDomElement('div#mydiv');
53+
expect(el).toBeDefined();
54+
expect(el?.tagName).toEqual('DIV');
55+
expect(el?.id).toEqual('mydiv');
56+
});
57+
});

0 commit comments

Comments
 (0)