diff --git a/packages/astro/package.json b/packages/astro/package.json index 5a065208fe50..96e8a48c0a8b 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -19,6 +19,13 @@ "peerDependencies": { "astro": "1.x" }, + "dependencies": { + "@sentry/browser": "7.73.0", + "@sentry/node": "7.73.0", + "@sentry/core": "7.73.0", + "@sentry/utils": "7.73.0", + "@sentry/types": "7.73.0" + }, "devDependencies": { "astro": "^3.2.3", "rollup": "^3.20.2", diff --git a/packages/astro/src/client/sdk.ts b/packages/astro/src/client/sdk.ts new file mode 100644 index 000000000000..aa32e9dcc095 --- /dev/null +++ b/packages/astro/src/client/sdk.ts @@ -0,0 +1,42 @@ +import type { BrowserOptions } from '@sentry/browser'; +import { BrowserTracing, init as initBrowserSdk } from '@sentry/browser'; +import { configureScope, hasTracingEnabled } from '@sentry/core'; +import { addOrUpdateIntegration } from '@sentry/utils'; + +import { applySdkMetadata } from '../common/metadata'; + +// Treeshakable guard to remove all code related to tracing +declare const __SENTRY_TRACING__: boolean; + +/** + * Initialize the client side of the Sentry Astro SDK. + * + * @param options Configuration options for the SDK. + */ +export function init(options: BrowserOptions): void { + applySdkMetadata(options, ['astro', 'browser']); + + addClientIntegrations(options); + + initBrowserSdk(options); + + configureScope(scope => { + scope.setTag('runtime', 'browser'); + }); +} + +function addClientIntegrations(options: BrowserOptions): void { + let integrations = options.integrations || []; + + // This evaluates to true unless __SENTRY_TRACING__ is text-replaced with "false", + // in which case everything inside will get treeshaken away + if (typeof __SENTRY_TRACING__ === 'undefined' || __SENTRY_TRACING__) { + if (hasTracingEnabled(options)) { + const defaultBrowserTracingIntegration = new BrowserTracing({}); + + integrations = addOrUpdateIntegration(defaultBrowserTracingIntegration, integrations); + } + } + + options.integrations = integrations; +} diff --git a/packages/astro/src/common/metadata.ts b/packages/astro/src/common/metadata.ts new file mode 100644 index 000000000000..ddd53f27362a --- /dev/null +++ b/packages/astro/src/common/metadata.ts @@ -0,0 +1,31 @@ +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 and SvelteKit. + * We don't extract it for bundle size reasons. + * @see https://github.com/getsentry/sentry-javascript/pull/7404 + * @see https://github.com/getsentry/sentry-javascript/pull/4196 + * + * If you make changes to this function consider updating the others 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.astro', + packages: names.map(name => ({ + name: `${PACKAGE_NAME_PREFIX}${name}`, + version: SDK_VERSION, + })), + version: SDK_VERSION, + } as SdkInfo); +} diff --git a/packages/astro/src/index.client.ts b/packages/astro/src/index.client.ts index dea210bd3fb8..2b85c05c3af1 100644 --- a/packages/astro/src/index.client.ts +++ b/packages/astro/src/index.client.ts @@ -1 +1,3 @@ -export const client = true; +export * from '@sentry/browser'; + +export { init } from './client/sdk'; diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 126a907bffce..2e008583cbe8 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -1 +1,60 @@ -export const server = true; +// Node SDK exports +// Unfortunately, we cannot `export * from '@sentry/node'` because in prod builds, +// Vite puts these exports into a `default` property (Sentry.default) rather than +// on the top - level namespace. +// Hence, we export everything from the Node SDK explicitly: +export { + addGlobalEventProcessor, + addBreadcrumb, + captureException, + captureEvent, + captureMessage, + captureCheckIn, + configureScope, + createTransport, + extractTraceparentData, + getActiveTransaction, + getHubFromCarrier, + getCurrentHub, + Hub, + makeMain, + Scope, + startTransaction, + SDK_VERSION, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + spanStatusfromHttpCode, + trace, + withScope, + autoDiscoverNodePerformanceMonitoringIntegrations, + makeNodeTransport, + defaultIntegrations, + defaultStackParser, + lastEventId, + flush, + close, + getSentryRelease, + addRequestDataToEvent, + DEFAULT_USER_INCLUDES, + extractRequestData, + deepReadDirSync, + Integrations, + Handlers, + setMeasurement, + getActiveSpan, + startSpan, + // eslint-disable-next-line deprecation/deprecation + startActiveSpan, + startInactiveSpan, + startSpanManual, + continueTrace, +} from '@sentry/node'; + +// We can still leave this for the carrier init and type exports +export * from '@sentry/node'; + +export { init } from './server/sdk'; diff --git a/packages/astro/src/index.types.ts b/packages/astro/src/index.types.ts index 6c4477dd4f04..e8ff7457f597 100644 --- a/packages/astro/src/index.types.ts +++ b/packages/astro/src/index.types.ts @@ -1 +1,25 @@ -export type Placeholder = true; +/* eslint-disable import/export */ + +// We export everything from both the client part of the SDK and from the server part. +// Some of the exports collide, which is not allowed, unless we redifine the colliding +// exports in this file - which we do below. +export * from './index.client'; +export * from './index.server'; + +import type { Integration, Options, StackParser } from '@sentry/types'; + +import type * as clientSdk from './index.client'; +import type * as serverSdk from './index.server'; + +/** Initializes Sentry Astro SDK */ +export declare function init(options: Options | clientSdk.BrowserOptions | serverSdk.NodeOptions): void; + +// We export a merged Integrations object so that users can (at least typing-wise) use all integrations everywhere. +export declare const Integrations: typeof clientSdk.Integrations & typeof serverSdk.Integrations; + +export declare const defaultIntegrations: Integration[]; +export declare const defaultStackParser: StackParser; + +export declare function close(timeout?: number | undefined): PromiseLike; +export declare function flush(timeout?: number | undefined): PromiseLike; +export declare function lastEventId(): string | undefined; diff --git a/packages/astro/src/server/sdk.ts b/packages/astro/src/server/sdk.ts new file mode 100644 index 000000000000..8c867ca46fc2 --- /dev/null +++ b/packages/astro/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, ['astro', 'node']); + + initNodeSdk(options); + + configureScope(scope => { + scope.setTag('runtime', 'node'); + }); +} diff --git a/packages/astro/test/client/sdk.test.ts b/packages/astro/test/client/sdk.test.ts new file mode 100644 index 000000000000..74a4dc4562ef --- /dev/null +++ b/packages/astro/test/client/sdk.test.ts @@ -0,0 +1,124 @@ +import type { BrowserClient } from '@sentry/browser'; +import * as SentryBrowser from '@sentry/browser'; +import { BrowserTracing, getCurrentHub, SDK_VERSION, WINDOW } from '@sentry/browser'; +import { vi } from 'vitest'; + +import { init } from '../../../astro/src/client/sdk'; + +const browserInit = vi.spyOn(SentryBrowser, 'init'); + +describe('Sentry client SDK', () => { + describe('init', () => { + afterEach(() => { + vi.clearAllMocks(); + WINDOW.__SENTRY__.hub = undefined; + }); + + it('adds Astro metadata to the SDK options', () => { + expect(browserInit).not.toHaveBeenCalled(); + + init({}); + + expect(browserInit).toHaveBeenCalledTimes(1); + expect(browserInit).toHaveBeenCalledWith( + expect.objectContaining({ + _metadata: { + sdk: { + name: 'sentry.javascript.astro', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/astro', version: SDK_VERSION }, + { name: 'npm:@sentry/browser', version: SDK_VERSION }, + ], + }, + }, + }), + ); + }); + + it('sets the runtime tag on the scope', () => { + const currentScope = getCurrentHub().getScope(); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({}); + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({ runtime: 'browser' }); + }); + + describe('automatically adds integrations', () => { + it.each([ + ['tracesSampleRate', { tracesSampleRate: 0 }], + ['tracesSampler', { tracesSampler: () => 1.0 }], + ['enableTracing', { enableTracing: true }], + ])('adds the BrowserTracing integration if tracing is enabled via %s', (_, tracingOptions) => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + ...tracingOptions, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing'); + + expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeDefined(); + }); + + it.each([ + ['enableTracing', { enableTracing: false }], + ['no tracing option set', {}], + ])("doesn't add the BrowserTracing integration if tracing is disabled via %s", (_, tracingOptions) => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + ...tracingOptions, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing'); + + expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeUndefined(); + }); + + it("doesn't add the BrowserTracing integration if `__SENTRY_TRACING__` is set to false", () => { + globalThis.__SENTRY_TRACING__ = false; + + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + enableTracing: true, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById('BrowserTracing'); + + expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeUndefined(); + + delete globalThis.__SENTRY_TRACING__; + }); + + it('Overrides the automatically default BrowserTracing instance with a a user-provided instance', () => { + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new BrowserTracing({ finalTimeout: 10, startTransactionOnLocationChange: false })], + enableTracing: true, + }); + + const integrationsToInit = browserInit.mock.calls[0][0]?.integrations; + + const browserTracing = (getCurrentHub().getClient() as BrowserClient)?.getIntegrationById( + 'BrowserTracing', + ) as BrowserTracing; + const options = browserTracing.options; + + expect(integrationsToInit).toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); + expect(browserTracing).toBeDefined(); + + // This shows that the user-configured options are still here + expect(options.finalTimeout).toEqual(10); + }); + }); + }); +}); diff --git a/packages/astro/test/server/sdk.test.ts b/packages/astro/test/server/sdk.test.ts new file mode 100644 index 000000000000..0e178f7ae45a --- /dev/null +++ b/packages/astro/test/server/sdk.test.ts @@ -0,0 +1,52 @@ +import { getCurrentHub } from '@sentry/core'; +import * as SentryNode from '@sentry/node'; +import { SDK_VERSION } from '@sentry/node'; +import { GLOBAL_OBJ } from '@sentry/utils'; +import { vi } from 'vitest'; + +import { init } from '../../src/server/sdk'; + +const nodeInit = vi.spyOn(SentryNode, 'init'); + +describe('Sentry server SDK', () => { + describe('init', () => { + afterEach(() => { + vi.clearAllMocks(); + GLOBAL_OBJ.__SENTRY__.hub = undefined; + }); + + it('adds Astro metadata to the SDK options', () => { + expect(nodeInit).not.toHaveBeenCalled(); + + init({}); + + expect(nodeInit).toHaveBeenCalledTimes(1); + expect(nodeInit).toHaveBeenCalledWith( + expect.objectContaining({ + _metadata: { + sdk: { + name: 'sentry.javascript.astro', + version: SDK_VERSION, + packages: [ + { name: 'npm:@sentry/astro', version: SDK_VERSION }, + { name: 'npm:@sentry/node', version: SDK_VERSION }, + ], + }, + }, + }), + ); + }); + + it('sets the runtime tag on the scope', () => { + const currentScope = getCurrentHub().getScope(); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({}); + + init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); + + // @ts-expect-error need access to protected _tags attribute + expect(currentScope._tags).toEqual({ runtime: 'node' }); + }); + }); +}); diff --git a/packages/sveltekit/src/common/metadata.ts b/packages/sveltekit/src/common/metadata.ts index 76a9642ee36b..d6acf72510cb 100644 --- a/packages/sveltekit/src/common/metadata.ts +++ b/packages/sveltekit/src/common/metadata.ts @@ -8,7 +8,7 @@ const PACKAGE_NAME_PREFIX = 'npm:@sentry/'; * * 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. + * If you make changes to this function consider updating the others as well. * * @param options SDK options object that gets mutated * @param names list of package names