diff --git a/packages/svelte/src/sdk.ts b/packages/svelte/src/sdk.ts index 448ccacbf046..3a7c671a7d1d 100644 --- a/packages/svelte/src/sdk.ts +++ b/packages/svelte/src/sdk.ts @@ -7,7 +7,7 @@ import { getDomElement } from '@sentry/utils'; */ export function init(options: BrowserOptions): void { options._metadata = options._metadata || {}; - options._metadata.sdk = { + options._metadata.sdk = options._metadata.sdk || { name: 'sentry.javascript.svelte', packages: [ { diff --git a/packages/svelte/test/sdk.test.ts b/packages/svelte/test/sdk.test.ts index 8203b025a437..ea3e3e383eba 100644 --- a/packages/svelte/test/sdk.test.ts +++ b/packages/svelte/test/sdk.test.ts @@ -1,23 +1,21 @@ -import { addGlobalEventProcessor, init as browserInit, SDK_VERSION } from '@sentry/browser'; +import { SDK_VERSION } from '@sentry/browser'; +import * as SentryBrowser from '@sentry/browser'; import type { EventProcessor } from '@sentry/types'; import { detectAndReportSvelteKit, init as svelteInit, isSvelteKitApp } from '../src/sdk'; let passedEventProcessor: EventProcessor | undefined; -jest.mock('@sentry/browser', () => { - const actual = jest.requireActual('@sentry/browser'); - return { - ...actual, - init: jest.fn().mockImplementation(actual.init), - addGlobalEventProcessor: jest.fn().mockImplementation(proc => { - passedEventProcessor = proc; - }), - }; -}); +const browserInit = jest.spyOn(SentryBrowser, 'init'); +const addGlobalEventProcessor = jest + .spyOn(SentryBrowser, 'addGlobalEventProcessor') + .mockImplementation((eventProcessor: EventProcessor) => { + passedEventProcessor = eventProcessor; + return () => {}; + }); describe('Initialize Svelte SDk', () => { - afterAll(() => { + beforeEach(() => { jest.clearAllMocks(); }); @@ -37,13 +35,45 @@ describe('Initialize Svelte SDk', () => { }; expect(browserInit).toHaveBeenCalledTimes(1); - expect(browserInit).toHaveBeenCalledWith(expect.objectContaining(expectedMetadata)); + expect(browserInit).toHaveBeenLastCalledWith(expect.objectContaining(expectedMetadata)); + }); + + it("doesn't add the default svelte metadata, if metadata is already passed", () => { + svelteInit({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + _metadata: { + sdk: { + name: 'sentry.javascript.sveltekit', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/sveltekit', version: SDK_VERSION }, + { name: 'npm:@sentry/svelte', version: SDK_VERSION }, + ], + }, + }, + }); + + expect(browserInit).toHaveBeenCalledTimes(1); + expect(browserInit).toHaveBeenLastCalledWith( + expect.objectContaining({ + _metadata: { + sdk: { + name: 'sentry.javascript.sveltekit', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/sveltekit', version: SDK_VERSION }, + { name: 'npm:@sentry/svelte', version: SDK_VERSION }, + ], + }, + }, + }), + ); }); }); describe('detectAndReportSvelteKit()', () => { const originalHtmlBody = document.body.innerHTML; - afterEach(() => { + beforeEach(() => { jest.clearAllMocks(); document.body.innerHTML = originalHtmlBody; passedEventProcessor = undefined; diff --git a/packages/sveltekit/src/client/index.ts b/packages/sveltekit/src/client/index.ts index cdd2d7ba8a6e..92f3f38bcd88 100644 --- a/packages/sveltekit/src/client/index.ts +++ b/packages/sveltekit/src/client/index.ts @@ -1,4 +1,3 @@ export * from '@sentry/svelte'; -// Just here so that eslint is happy until we export more stuff here -export const PLACEHOLDER_CLIENT = 'PLACEHOLDER'; +export { init } from './sdk'; diff --git a/packages/sveltekit/src/client/sdk.ts b/packages/sveltekit/src/client/sdk.ts new file mode 100644 index 000000000000..d1f0f9ac597f --- /dev/null +++ b/packages/sveltekit/src/client/sdk.ts @@ -0,0 +1,18 @@ +import type { BrowserOptions } from '@sentry/svelte'; +import { configureScope, init as initSvelteSdk } from '@sentry/svelte'; + +import { applySdkMetadata } from '../common/metadata'; + +/** + * + * @param options + */ +export function init(options: BrowserOptions): void { + applySdkMetadata(options, ['sveltekit', 'svelte']); + + initSvelteSdk(options); + + configureScope(scope => { + scope.setTag('runtime', 'browser'); + }); +} diff --git a/packages/sveltekit/src/common/metadata.ts b/packages/sveltekit/src/common/metadata.ts new file mode 100644 index 000000000000..76a9642ee36b --- /dev/null +++ b/packages/sveltekit/src/common/metadata.ts @@ -0,0 +1,28 @@ +import { SDK_VERSION } from '@sentry/core'; +import type { Options, SdkInfo } from '@sentry/types'; + +const PACKAGE_NAME_PREFIX = 'npm:@sentry/'; + +/** + * A builder for the SDK metadata in the options for the SDK initialization. + * + * Note: This function is identical to `buildMetadata` in Remix and NextJS. + * We don't extract it for bundle size reasons. + * If you make changes to this function consider updating the othera as well. + * + * @param options SDK options object that gets mutated + * @param names list of package names + */ +export function applySdkMetadata(options: Options, names: string[]): void { + options._metadata = options._metadata || {}; + options._metadata.sdk = + options._metadata.sdk || + ({ + name: 'sentry.javascript.sveltekit', + packages: names.map(name => ({ + name: `${PACKAGE_NAME_PREFIX}${name}`, + version: SDK_VERSION, + })), + version: SDK_VERSION, + } as SdkInfo); +} diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 04654fecd19a..6ac8d97b4241 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -1,4 +1,3 @@ export * from '@sentry/node'; -// Just here so that eslint is happy until we export more stuff here -export const PLACEHOLDER_SERVER = 'PLACEHOLDER'; +export { init } from './sdk'; diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts new file mode 100644 index 000000000000..1d1bbdbf2674 --- /dev/null +++ b/packages/sveltekit/src/server/sdk.ts @@ -0,0 +1,19 @@ +import { configureScope } from '@sentry/core'; +import type { NodeOptions } from '@sentry/node'; +import { init as initNodeSdk } from '@sentry/node'; + +import { applySdkMetadata } from '../common/metadata'; + +/** + * + * @param options + */ +export function init(options: NodeOptions): void { + applySdkMetadata(options, ['sveltekit', 'node']); + + initNodeSdk(options); + + configureScope(scope => { + scope.setTag('runtime', 'node'); + }); +} diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts new file mode 100644 index 000000000000..5ed924a60ccc --- /dev/null +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -0,0 +1,49 @@ +import { getCurrentHub } from '@sentry/core'; +import * as SentrySvelte from '@sentry/svelte'; +import { SDK_VERSION, WINDOW } from '@sentry/svelte'; + +import { init } from '../../src/client/sdk'; +const svelteInit = jest.spyOn(SentrySvelte, 'init'); + +describe('Sentry client SDK', () => { + describe('init', () => { + afterEach(() => { + jest.clearAllMocks(); + WINDOW.__SENTRY__.hub = undefined; + }); + + it('adds SvelteKit metadata to the SDK options', () => { + expect(svelteInit).not.toHaveBeenCalled(); + + init({}); + + expect(svelteInit).toHaveBeenCalledTimes(1); + expect(svelteInit).toHaveBeenCalledWith( + expect.objectContaining({ + _metadata: { + sdk: { + name: 'sentry.javascript.sveltekit', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/sveltekit', version: SDK_VERSION }, + { name: 'npm:@sentry/svelte', version: SDK_VERSION }, + ], + }, + }, + }), + ); + }); + + it('sets the runtime tag on the scope', () => { + const currentScope = getCurrentHub().getScope(); + + // @ts-ignore need access to protected _tags attribute + expect(currentScope._tags).toEqual({}); + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + // @ts-ignore need access to protected _tags attribute + expect(currentScope._tags).toEqual({ runtime: 'browser' }); + }); + }); +}); diff --git a/packages/sveltekit/test/server/sdk.test.ts b/packages/sveltekit/test/server/sdk.test.ts new file mode 100644 index 000000000000..785cf158e5c8 --- /dev/null +++ b/packages/sveltekit/test/server/sdk.test.ts @@ -0,0 +1,51 @@ +import { getCurrentHub } from '@sentry/core'; +import * as SentryNode from '@sentry/node'; +import { SDK_VERSION } from '@sentry/node'; +import { GLOBAL_OBJ } from '@sentry/utils'; + +import { init } from '../../src/server/sdk'; + +const nodeInit = jest.spyOn(SentryNode, 'init'); + +describe('Sentry server SDK', () => { + describe('init', () => { + afterEach(() => { + jest.clearAllMocks(); + GLOBAL_OBJ.__SENTRY__.hub = undefined; + }); + + it('adds SvelteKit metadata to the SDK options', () => { + expect(nodeInit).not.toHaveBeenCalled(); + + init({}); + + expect(nodeInit).toHaveBeenCalledTimes(1); + expect(nodeInit).toHaveBeenCalledWith( + expect.objectContaining({ + _metadata: { + sdk: { + name: 'sentry.javascript.sveltekit', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/sveltekit', version: SDK_VERSION }, + { name: 'npm:@sentry/node', version: SDK_VERSION }, + ], + }, + }, + }), + ); + }); + + it('sets the runtime tag on the scope', () => { + const currentScope = getCurrentHub().getScope(); + + // @ts-ignore need access to protected _tags attribute + expect(currentScope._tags).toEqual({}); + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + // @ts-ignore need access to protected _tags attribute + expect(currentScope._tags).toEqual({ runtime: 'node' }); + }); + }); +});