diff --git a/packages/svelte/src/sdk.ts b/packages/svelte/src/sdk.ts
index 59108e903151..4857163ec137 100644
--- a/packages/svelte/src/sdk.ts
+++ b/packages/svelte/src/sdk.ts
@@ -1,5 +1,6 @@
-import { BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browser';
-
+import { addGlobalEventProcessor, BrowserOptions, init as browserInit, SDK_VERSION } from '@sentry/browser';
+import type { EventProcessor } from '@sentry/types';
+import { getDomElement } from '@sentry/utils';
/**
* Inits the Svelte SDK
*/
@@ -17,4 +18,45 @@ export function init(options: BrowserOptions): void {
};
browserInit(options);
+
+ detectAndReportSvelteKit();
+}
+
+/**
+ * Adds a global event processor to detect if the SDK is initialized in a SvelteKit frontend,
+ * in which case we add SvelteKit an event.modules entry to outgoing events.
+ * SvelteKit detection is performed only once, when the event processor is called for the
+ * first time. We cannot perform this check upfront (directly when init is called) because
+ * at this time, the HTML element might not yet be accessible.
+ */
+export function detectAndReportSvelteKit(): void {
+ let detectedSvelteKit: boolean | undefined = undefined;
+
+ const svelteKitProcessor: EventProcessor = event => {
+ if (detectedSvelteKit === undefined) {
+ detectedSvelteKit = isSvelteKitApp();
+ }
+ if (detectedSvelteKit) {
+ event.modules = {
+ svelteKit: 'latest',
+ ...event.modules,
+ };
+ }
+ return event;
+ };
+ svelteKitProcessor.id = 'svelteKitProcessor';
+
+ addGlobalEventProcessor(svelteKitProcessor);
+}
+
+/**
+ * To actually detect a SvelteKit frontend, we search the DOM for a special
+ * div that's inserted by SvelteKit when the page is rendered. It's identifyed
+ * by its id, 'svelte-announcer', and it's used to improve page accessibility.
+ * This div is not present when only using Svelte without SvelteKit.
+ *
+ * @see https://github.com/sveltejs/kit/issues/307 for more information
+ */
+export function isSvelteKitApp(): boolean {
+ return getDomElement('div#svelte-announcer') !== null;
}
diff --git a/packages/svelte/test/sdk.test.ts b/packages/svelte/test/sdk.test.ts
index 8ca58bf93bca..8306cb1f3bd9 100644
--- a/packages/svelte/test/sdk.test.ts
+++ b/packages/svelte/test/sdk.test.ts
@@ -1,8 +1,15 @@
-import { init as browserInitRaw, SDK_VERSION } from '@sentry/browser';
+import { addGlobalEventProcessor, init as browserInitRaw, SDK_VERSION } from '@sentry/browser';
+import { EventProcessor } from '@sentry/types';
-import { init as svelteInit } from '../src/sdk';
+import { detectAndReportSvelteKit, init as svelteInit, isSvelteKitApp } from '../src/sdk';
const browserInit = browserInitRaw as jest.Mock;
+const addGlobalEventProcessorFunction = addGlobalEventProcessor as jest.Mock;
+let passedEventProcessor: EventProcessor | undefined;
+addGlobalEventProcessorFunction.mockImplementation(proc => {
+ passedEventProcessor = proc;
+});
+
jest.mock('@sentry/browser');
describe('Initialize Svelte SDk', () => {
@@ -29,3 +36,54 @@ describe('Initialize Svelte SDk', () => {
expect(browserInit).toHaveBeenCalledWith(expect.objectContaining(expectedMetadata));
});
});
+
+describe('detectAndReportSvelteKit()', () => {
+ const originalHtmlBody = document.body.innerHTML;
+ afterEach(() => {
+ jest.clearAllMocks();
+ document.body.innerHTML = originalHtmlBody;
+ passedEventProcessor = undefined;
+ });
+
+ it('registers a global event processor', async () => {
+ detectAndReportSvelteKit();
+
+ expect(addGlobalEventProcessorFunction).toHaveBeenCalledTimes(1);
+ expect(passedEventProcessor?.id).toEqual('svelteKitProcessor');
+ });
+
+ it('adds "SvelteKit" as a module to the event, if SvelteKit was detected', () => {
+ document.body.innerHTML += '
Home
';
+ detectAndReportSvelteKit();
+
+ const processedEvent = passedEventProcessor && passedEventProcessor({} as unknown as any, {});
+
+ expect(processedEvent).toBeDefined();
+ expect(processedEvent).toEqual({ modules: { svelteKit: 'latest' } });
+ });
+
+ it("doesn't add anything to the event, if SvelteKit was not detected", () => {
+ document.body.innerHTML = '';
+ detectAndReportSvelteKit();
+
+ const processedEvent = passedEventProcessor && passedEventProcessor({} as unknown as any, {});
+
+ expect(processedEvent).toBeDefined();
+ expect(processedEvent).toEqual({});
+ });
+
+ describe('isSvelteKitApp()', () => {
+ it('returns true if the svelte-announcer div is present', () => {
+ document.body.innerHTML += 'Home
';
+ expect(isSvelteKitApp()).toBe(true);
+ });
+ it('returns false if the svelte-announcer div is not present (but similar elements)', () => {
+ document.body.innerHTML += 'Home
';
+ expect(isSvelteKitApp()).toBe(false);
+ });
+ it('returns false if no div is present', () => {
+ document.body.innerHTML = '';
+ expect(isSvelteKitApp()).toBe(false);
+ });
+ });
+});
diff --git a/packages/tracing/src/browser/browsertracing.ts b/packages/tracing/src/browser/browsertracing.ts
index 27b788aaed85..20ff9effdb0e 100644
--- a/packages/tracing/src/browser/browsertracing.ts
+++ b/packages/tracing/src/browser/browsertracing.ts
@@ -1,7 +1,7 @@
/* eslint-disable max-lines */
import { Hub } from '@sentry/hub';
import { EventProcessor, Integration, Transaction, TransactionContext } from '@sentry/types';
-import { getGlobalObject, logger, parseBaggageSetMutability } from '@sentry/utils';
+import { getDomElement, getGlobalObject, logger, parseBaggageSetMutability } from '@sentry/utils';
import { startIdleTransaction } from '../hubextensions';
import { DEFAULT_FINAL_TIMEOUT, DEFAULT_IDLE_TIMEOUT } from '../idletransaction';
@@ -294,13 +294,6 @@ export function extractTraceDataFromMetaTags(): Partial | un
/** Returns the value of a meta tag */
export function getMetaContent(metaName: string): string | null {
- const globalObject = getGlobalObject();
-
- // DOM/querySelector is not available in all environments
- if (globalObject.document && globalObject.document.querySelector) {
- const el = globalObject.document.querySelector(`meta[name=${metaName}]`);
- return el ? el.getAttribute('content') : null;
- } else {
- return null;
- }
+ const metaTag = getDomElement(`meta[name=${metaName}]`);
+ return metaTag ? metaTag.getAttribute('content') : null;
}
diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts
index aeb6f9a33296..c11cf22950f8 100644
--- a/packages/utils/src/browser.ts
+++ b/packages/utils/src/browser.ts
@@ -122,3 +122,21 @@ export function getLocationHref(): string {
return '';
}
}
+
+/**
+ * Gets a DOM element by using document.querySelector.
+ *
+ * This wrapper will first check for the existance of the function before
+ * actually calling it so that we don't have to take care of this check,
+ * every time we want to access the DOM.
+ * Reason: DOM/querySelector is not available in all environments
+ *
+ * @param selector the selector string passed on to document.querySelector
+ */
+export function getDomElement(selector: string): Element | null {
+ const global = getGlobalObject();
+ if (global.document && global.document.querySelector) {
+ return global.document.querySelector(selector);
+ }
+ return null;
+}
diff --git a/packages/utils/test/browser.test.ts b/packages/utils/test/browser.test.ts
index 1b7978b75ba6..ca5513cc4f0b 100644
--- a/packages/utils/test/browser.test.ts
+++ b/packages/utils/test/browser.test.ts
@@ -1,6 +1,6 @@
import { JSDOM } from 'jsdom';
-import { htmlTreeAsString } from '../src/browser';
+import { getDomElement, htmlTreeAsString } from '../src/browser';
beforeAll(() => {
const dom = new JSDOM();
@@ -45,3 +45,13 @@ describe('htmlTreeAsString', () => {
);
});
});
+
+describe('getDomElement', () => {
+ it('returns the element for a given query selector', () => {
+ document.head.innerHTML = 'Hello
';
+ const el = getDomElement('div#mydiv');
+ expect(el).toBeDefined();
+ expect(el?.tagName).toEqual('DIV');
+ expect(el?.id).toEqual('mydiv');
+ });
+});