diff --git a/packages/sveltekit/src/client/load.ts b/packages/sveltekit/src/client/load.ts index 32f310e3bd2d..e7e9a6673999 100644 --- a/packages/sveltekit/src/client/load.ts +++ b/packages/sveltekit/src/client/load.ts @@ -1,7 +1,7 @@ import { trace } from '@sentry/core'; import { captureException } from '@sentry/svelte'; import { addExceptionMechanism, objectify } from '@sentry/utils'; -import type { Load } from '@sveltejs/kit'; +import type { LoadEvent } from '@sveltejs/kit'; function sendErrorToSentry(e: unknown): unknown { // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can @@ -27,14 +27,18 @@ function sendErrorToSentry(e: unknown): unknown { } /** - * Wrap load function with Sentry - * - * @param origLoad SvelteKit user defined load function + * @inheritdoc */ -export function wrapLoadWithSentry(origLoad: Load): Load { +// The liberal generic typing of `T` is necessary because we cannot let T extend `Load`. +// This function needs to tell TS that it returns exactly the type that it was called with +// because SvelteKit generates the narrowed down `PageLoad` or `LayoutLoad` types +// at build time for every route. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wrapLoadWithSentry any>(origLoad: T): T { return new Proxy(origLoad, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - const [event] = args; + apply: (wrappingTarget, thisArg, args: Parameters) => { + // Type casting here because `T` cannot extend `Load` (see comment above function signature) + const event = args[0] as LoadEvent; const routeId = event.route.id; return trace( diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 1c1c0576729f..c29315bc0181 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -9,7 +9,7 @@ export * from './server'; import type { Integration, Options, StackParser } from '@sentry/types'; // eslint-disable-next-line import/no-unresolved -import type { HandleClientError, HandleServerError, Load, ServerLoad } from '@sveltejs/kit'; +import type { HandleClientError, HandleServerError } from '@sveltejs/kit'; import type * as clientSdk from './client'; import type * as serverSdk from './server'; @@ -21,7 +21,25 @@ export declare function handleErrorWithSentry; -export declare function wrapLoadWithSentry(origLoad: S): S; +/** + * Wrap a universal load function (e.g. +page.js or +layout.js) with Sentry functionality + * + * Usage: + * + * ```js + * // +page.js + * + * import { wrapLoadWithSentry } + * + * export const load = wrapLoadWithSentry((event) => { + * // your load code + * }); + * ``` + * + * @param origLoad SvelteKit user defined universal `load` function + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export declare function wrapLoadWithSentry any>(origLoad: T): T; // 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; diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index fa6c10648741..cc291e63f579 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -2,16 +2,12 @@ import type { Span } from '@sentry/core'; import { getActiveTransaction, getCurrentHub, trace } from '@sentry/core'; import { captureException } from '@sentry/node'; -import { - addExceptionMechanism, - baggageHeaderToDynamicSamplingContext, - dynamicSamplingContextToSentryBaggageHeader, - extractTraceparentData, - objectify, -} from '@sentry/utils'; +import { addExceptionMechanism, dynamicSamplingContextToSentryBaggageHeader, objectify } from '@sentry/utils'; import type { Handle, ResolveOptions } from '@sveltejs/kit'; import * as domain from 'domain'; +import { getTracePropagationData } from './utils'; + function sendErrorToSentry(e: unknown): unknown { // In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can // store a seen flag on it. @@ -77,10 +73,7 @@ export const sentryHandle: Handle = input => { }; function instrumentHandle({ event, resolve }: Parameters[0]): ReturnType { - const sentryTraceHeader = event.request.headers.get('sentry-trace'); - const baggageHeader = event.request.headers.get('baggage'); - const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined; - const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); + const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event); return trace( { diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 9109f29499d4..1307a22e2846 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -2,5 +2,5 @@ export * from '@sentry/node'; export { init } from './sdk'; export { handleErrorWithSentry } from './handleError'; -export { wrapLoadWithSentry } from './load'; +export { wrapLoadWithSentry, wrapServerLoadWithSentry } from './load'; export { sentryHandle } from './handle'; diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server/load.ts index 068a1f4d0f8d..a050a5e904ba 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server/load.ts @@ -1,14 +1,11 @@ /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ import { trace } from '@sentry/core'; import { captureException } from '@sentry/node'; -import type { DynamicSamplingContext, TraceparentData, TransactionContext } from '@sentry/types'; -import { - addExceptionMechanism, - baggageHeaderToDynamicSamplingContext, - extractTraceparentData, - objectify, -} from '@sentry/utils'; -import type { HttpError, Load, LoadEvent, ServerLoad, ServerLoadEvent } from '@sveltejs/kit'; +import type { TransactionContext } from '@sentry/types'; +import { addExceptionMechanism, objectify } from '@sentry/utils'; +import type { HttpError, LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; + +import { getTracePropagationData } from './utils'; function isHttpError(err: unknown): err is HttpError { return typeof err === 'object' && err !== null && 'status' in err && 'body' in err; @@ -45,27 +42,27 @@ function sendErrorToSentry(e: unknown): unknown { } /** - * Wrap load function with Sentry - * - * @param origLoad SvelteKit user defined load function + * @inheritdoc */ -export function wrapLoadWithSentry(origLoad: T): T { +// The liberal generic typing of `T` is necessary because we cannot let T extend `Load`. +// This function needs to tell TS that it returns exactly the type that it was called with +// because SvelteKit generates the narrowed down `PageLoad` or `LayoutLoad` types +// at build time for every route. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wrapLoadWithSentry any>(origLoad: T): T { return new Proxy(origLoad, { - apply: (wrappingTarget, thisArg, args: Parameters) => { - const [event] = args; + apply: (wrappingTarget, thisArg, args: Parameters) => { + // Type casting here because `T` cannot extend `Load` (see comment above function signature) + const event = args[0] as LoadEvent; const routeId = event.route && event.route.id; - const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event); - const traceLoadContext: TransactionContext = { - op: `function.sveltekit${isServerOnlyLoad(event) ? '.server' : ''}.load`, + op: 'function.sveltekit.load', name: routeId ? routeId : event.url.pathname, status: 'ok', metadata: { source: routeId ? 'route' : 'url', - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, }, - ...traceparentData, }; return trace(traceLoadContext, () => wrappingTarget.apply(thisArg, args), sendErrorToSentry); @@ -73,30 +70,49 @@ export function wrapLoadWithSentry(origLoad: T): T }); } -function getTracePropagationData(event: ServerLoadEvent | LoadEvent): { - traceparentData?: TraceparentData; - dynamicSamplingContext?: Partial; -} { - if (!isServerOnlyLoad(event)) { - return {}; - } - - const sentryTraceHeader = event.request.headers.get('sentry-trace'); - const baggageHeader = event.request.headers.get('baggage'); - const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined; - const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); - - return { traceparentData, dynamicSamplingContext }; -} - /** - * Our server-side wrapLoadWithSentry can be used to wrap two different kinds of `load` functions: - * - load functions from `+(page|layout).ts`: These can be called both on client and on server - * - load functions from `+(page|layout).server.ts`: These are only called on the server + * Wrap a server-only load function (e.g. +page.server.js or +layout.server.js) with Sentry functionality + * + * Usage: + * + * ```js + * // +page.serverjs * - * In both cases, load events look differently. We can distinguish them by checking if the - * event has a `request` field (which only the server-exclusive load event has). + * import { wrapServerLoadWithSentry } + * + * export const load = wrapServerLoadWithSentry((event) => { + * // your load code + * }); + * ``` + * + * @param origServerLoad SvelteKit user defined server-only load function */ -function isServerOnlyLoad(event: ServerLoadEvent | LoadEvent): event is ServerLoadEvent { - return 'request' in event; +// The liberal generic typing of `T` is necessary because we cannot let T extend `ServerLoad`. +// This function needs to tell TS that it returns exactly the type that it was called with +// because SvelteKit generates the narrowed down `PageServerLoad` or `LayoutServerLoad` types +// at build time for every route. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function wrapServerLoadWithSentry any>(origServerLoad: T): T { + return new Proxy(origServerLoad, { + apply: (wrappingTarget, thisArg, args: Parameters) => { + // Type casting here because `T` cannot extend `ServerLoad` (see comment above function signature) + const event = args[0] as ServerLoadEvent; + const routeId = event.route && event.route.id; + + const { dynamicSamplingContext, traceparentData } = getTracePropagationData(event); + + const traceLoadContext: TransactionContext = { + op: 'function.sveltekit.server.load', + name: routeId ? routeId : event.url.pathname, + status: 'ok', + metadata: { + source: routeId ? 'route' : 'url', + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + }, + ...traceparentData, + }; + + return trace(traceLoadContext, () => wrappingTarget.apply(thisArg, args), sendErrorToSentry); + }, + }); } diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server/utils.ts new file mode 100644 index 000000000000..67c3bfe9e050 --- /dev/null +++ b/packages/sveltekit/src/server/utils.ts @@ -0,0 +1,19 @@ +import type { DynamicSamplingContext, TraceparentData } from '@sentry/types'; +import { baggageHeaderToDynamicSamplingContext, extractTraceparentData } from '@sentry/utils'; +import type { RequestEvent } from '@sveltejs/kit'; + +/** + * Takes a request event and extracts traceparent and DSC data + * from the `sentry-trace` and `baggage` DSC headers. + */ +export function getTracePropagationData(event: RequestEvent): { + traceparentData?: TraceparentData; + dynamicSamplingContext?: Partial; +} { + const sentryTraceHeader = event.request.headers.get('sentry-trace'); + const baggageHeader = event.request.headers.get('baggage'); + const traceparentData = sentryTraceHeader ? extractTraceparentData(sentryTraceHeader) : undefined; + const dynamicSamplingContext = baggageHeaderToDynamicSamplingContext(baggageHeader); + + return { traceparentData, dynamicSamplingContext }; +} diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index 69c31a74595d..2671b0d5d217 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -1,10 +1,10 @@ import { addTracingExtensions } from '@sentry/core'; import { Scope } from '@sentry/node'; -import type { ServerLoad } from '@sveltejs/kit'; +import type { Load, ServerLoad } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; import { vi } from 'vitest'; -import { wrapLoadWithSentry } from '../../src/server/load'; +import { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../../src/server/load'; const mockCaptureException = vi.fn(); let mockScope = new Scope(); @@ -114,16 +114,19 @@ beforeAll(() => { addTracingExtensions(); }); -describe('wrapLoadWithSentry', () => { - beforeEach(() => { - mockCaptureException.mockClear(); - mockAddExceptionMechanism.mockClear(); - mockTrace.mockClear(); - mockScope = new Scope(); - }); +beforeEach(() => { + mockCaptureException.mockClear(); + mockAddExceptionMechanism.mockClear(); + mockTrace.mockClear(); + mockScope = new Scope(); +}); +describe.each([ + ['wrapLoadWithSentry', wrapLoadWithSentry], + ['wrapServerLoadWithSentry', wrapServerLoadWithSentry], +])('Common functionality of load wrappers (%s) ', (_, sentryLoadWrapperFn) => { it('calls captureException', async () => { - async function load({ params }: Parameters[0]): Promise> { + async function load({ params }) { return { post: getById(params.id), }; @@ -151,12 +154,12 @@ describe('wrapLoadWithSentry', () => { [503, 1], [504, 1], ])('error with status code %s calls captureException %s times', async (code, times) => { - async function load({ params }: Parameters[0]): Promise> { + async function load({ params }) { throw error(code, params.id); } const wrappedLoad = wrapLoadWithSentry(load); - const res = wrappedLoad(MOCK_LOAD_ARGS); + const res = wrappedLoad({ ...MOCK_LOAD_ARGS }); await expect(res).rejects.toThrow(); expect(mockCaptureException).toHaveBeenCalledTimes(times); @@ -169,14 +172,14 @@ describe('wrapLoadWithSentry', () => { return mockScope; }); - async function load({ params }: Parameters[0]): Promise> { + async function load({ params }) { return { post: getById(params.id), }; } - const wrappedLoad = wrapLoadWithSentry(load); - const res = wrappedLoad(MOCK_LOAD_ARGS); + const wrappedLoad = sentryLoadWrapperFn.call(this, load); + const res = wrappedLoad(MOCK_SERVER_ONLY_LOAD_ARGS); await expect(res).rejects.toThrow(); expect(addEventProcessorSpy).toBeCalledTimes(1); @@ -186,125 +189,163 @@ describe('wrapLoadWithSentry', () => { { handled: false, type: 'sveltekit', data: { function: 'load' } }, ); }); +}); +describe('wrapLoadWithSentry calls trace', () => { + async function load({ params }: Parameters[0]): Promise> { + return { + post: params.id, + }; + } + + it('with the context of the universal load function', async () => { + const wrappedLoad = wrapLoadWithSentry(load); + await wrappedLoad(MOCK_LOAD_ARGS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith( + { + op: 'function.sveltekit.load', + name: '/users/[id]', + status: 'ok', + metadata: { + source: 'route', + }, + }, + expect.any(Function), + expect.any(Function), + ); + }); - describe('calls trace', () => { - async function load({ params }: Parameters[0]): Promise> { - return { - post: params.id, - }; - } + it('falls back to the raw url if `event.route.id` is not available', async () => { + const wrappedLoad = wrapLoadWithSentry(load); + await wrappedLoad(MOCK_LOAD_NO_ROUTE_ARGS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith( + { + op: 'function.sveltekit.load', + name: '/users/123', + status: 'ok', + metadata: { + source: 'url', + }, + }, + expect.any(Function), + expect.any(Function), + ); + }); +}); - describe('for server-only load', () => { - it('attaches trace data if available', async () => { - const wrappedLoad = wrapLoadWithSentry(load); - await wrappedLoad(MOCK_SERVER_ONLY_LOAD_ARGS); - - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( - { - op: 'function.sveltekit.server.load', - name: '/users/[id]', - parentSampled: true, - parentSpanId: '1234567890abcdef', - status: 'ok', - traceId: '1234567890abcdef1234567890abcdef', - metadata: { - dynamicSamplingContext: { - environment: 'production', - public_key: 'dogsarebadatkeepingsecrets', - release: '1.0.0', - sample_rate: '1', - trace_id: '1234567890abcdef1234567890abcdef', - transaction: 'dogpark', - user_segment: 'segmentA', - }, - source: 'route', - }, - }, - expect.any(Function), - expect.any(Function), - ); - }); - - it("doesn't attach trace data if it's not available", async () => { - const wrappedLoad = wrapLoadWithSentry(load); - await wrappedLoad(MOCK_SERVER_ONLY_NO_TRACE_LOAD_ARGS); - - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( - { - op: 'function.sveltekit.server.load', - name: '/users/[id]', - status: 'ok', - metadata: { - source: 'route', - }, - }, - expect.any(Function), - expect.any(Function), - ); - }); - - it("doesn't attach the DSC data if the baggage header not available", async () => { - const wrappedLoad = wrapLoadWithSentry(load); - await wrappedLoad(MOCK_SERVER_ONLY_NO_BAGGAGE_LOAD_ARGS); - - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( - { - op: 'function.sveltekit.server.load', - name: '/users/[id]', - parentSampled: true, - parentSpanId: '1234567890abcdef', - status: 'ok', - traceId: '1234567890abcdef1234567890abcdef', - metadata: { - dynamicSamplingContext: {}, - source: 'route', - }, +describe('wrapServerLoadWithSentry calls trace', () => { + async function serverLoad({ params }: Parameters[0]): Promise> { + return { + post: params.id, + }; + } + + it('attaches trace data if available', async () => { + const wrappedLoad = wrapServerLoadWithSentry(serverLoad); + await wrappedLoad(MOCK_SERVER_ONLY_LOAD_ARGS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith( + { + op: 'function.sveltekit.server.load', + name: '/users/[id]', + parentSampled: true, + parentSpanId: '1234567890abcdef', + status: 'ok', + traceId: '1234567890abcdef1234567890abcdef', + metadata: { + dynamicSamplingContext: { + environment: 'production', + public_key: 'dogsarebadatkeepingsecrets', + release: '1.0.0', + sample_rate: '1', + trace_id: '1234567890abcdef1234567890abcdef', + transaction: 'dogpark', + user_segment: 'segmentA', }, - expect.any(Function), - expect.any(Function), - ); - }); - }); + source: 'route', + }, + }, + expect.any(Function), + expect.any(Function), + ); + }); - it('for shared load', async () => { - const wrappedLoad = wrapLoadWithSentry(load); - await wrappedLoad(MOCK_LOAD_ARGS); - - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( - { - op: 'function.sveltekit.load', - name: '/users/[id]', - status: 'ok', - metadata: { - source: 'route', - }, + it("doesn't attach trace data if it's not available", async () => { + const wrappedLoad = wrapServerLoadWithSentry(serverLoad); + await wrappedLoad(MOCK_SERVER_ONLY_NO_TRACE_LOAD_ARGS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith( + { + op: 'function.sveltekit.server.load', + name: '/users/[id]', + status: 'ok', + metadata: { + source: 'route', }, - expect.any(Function), - expect.any(Function), - ); - }); + }, + expect.any(Function), + expect.any(Function), + ); + }); - it('falls back to the raw url if `event.route.id` is not available', async () => { - const wrappedLoad = wrapLoadWithSentry(load); - await wrappedLoad(MOCK_LOAD_NO_ROUTE_ARGS); - - expect(mockTrace).toHaveBeenCalledTimes(1); - expect(mockTrace).toHaveBeenCalledWith( - { - op: 'function.sveltekit.load', - name: '/users/123', - status: 'ok', - metadata: { - source: 'url', + it("doesn't attach the DSC data if the baggage header not available", async () => { + const wrappedLoad = wrapServerLoadWithSentry(serverLoad); + await wrappedLoad(MOCK_SERVER_ONLY_NO_BAGGAGE_LOAD_ARGS); + + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith( + { + op: 'function.sveltekit.server.load', + name: '/users/[id]', + parentSampled: true, + parentSpanId: '1234567890abcdef', + status: 'ok', + traceId: '1234567890abcdef1234567890abcdef', + metadata: { + dynamicSamplingContext: {}, + source: 'route', + }, + }, + expect.any(Function), + expect.any(Function), + ); + }); + + it('falls back to the raw url if `event.route.id` is not available', async () => { + const event = { ...MOCK_SERVER_ONLY_LOAD_ARGS }; + delete event.route; + const wrappedLoad = wrapServerLoadWithSentry(serverLoad); + await wrappedLoad(event); + + expect(mockTrace).toHaveBeenCalledTimes(1); + expect(mockTrace).toHaveBeenCalledWith( + { + op: 'function.sveltekit.server.load', + name: '/users/123', + parentSampled: true, + parentSpanId: '1234567890abcdef', + status: 'ok', + traceId: '1234567890abcdef1234567890abcdef', + metadata: { + dynamicSamplingContext: { + environment: 'production', + public_key: 'dogsarebadatkeepingsecrets', + release: '1.0.0', + sample_rate: '1', + trace_id: '1234567890abcdef1234567890abcdef', + transaction: 'dogpark', + user_segment: 'segmentA', }, + source: 'url', }, - expect.any(Function), - expect.any(Function), - ); - }); + }, + expect.any(Function), + expect.any(Function), + ); }); }); diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts new file mode 100644 index 000000000000..8e5c064c338c --- /dev/null +++ b/packages/sveltekit/test/server/utils.test.ts @@ -0,0 +1,55 @@ +import { getTracePropagationData } from '../../src/server/utils'; + +const MOCK_REQUEST_EVENT: any = { + request: { + headers: { + get: (key: string) => { + if (key === 'sentry-trace') { + return '1234567890abcdef1234567890abcdef-1234567890abcdef-1'; + } + + if (key === 'baggage') { + return ( + 'sentry-environment=production,sentry-release=1.0.0,sentry-transaction=dogpark,' + + 'sentry-user_segment=segmentA,sentry-public_key=dogsarebadatkeepingsecrets,' + + 'sentry-trace_id=1234567890abcdef1234567890abcdef,sentry-sample_rate=1' + ); + } + + return null; + }, + }, + }, +}; + +describe('getTracePropagationData', () => { + it('returns traceParentData and DSC if both are available', () => { + const event: any = MOCK_REQUEST_EVENT; + + const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event); + + expect(traceparentData).toEqual({ + parentSampled: true, + parentSpanId: '1234567890abcdef', + traceId: '1234567890abcdef1234567890abcdef', + }); + + expect(dynamicSamplingContext).toEqual({ + environment: 'production', + public_key: 'dogsarebadatkeepingsecrets', + release: '1.0.0', + sample_rate: '1', + trace_id: '1234567890abcdef1234567890abcdef', + transaction: 'dogpark', + user_segment: 'segmentA', + }); + }); + + it('returns undefined if the necessary header is not avaolable', () => { + const event: any = { request: { headers: { get: () => undefined } } }; + const { traceparentData, dynamicSamplingContext } = getTracePropagationData(event); + + expect(traceparentData).toBeUndefined(); + expect(dynamicSamplingContext).toBeUndefined(); + }); +});