();
-
- if (!client) {
- return;
- }
-
startBrowserTracingNavigationSpan(client, spanContext);
}
@@ -148,8 +150,9 @@ export function withSentry, R extends React.Co
const matches = _useMatches();
_useEffect(() => {
- if (matches && matches.length) {
- const routeName = matches[matches.length - 1].id;
+ const lastMatch = matches && matches[matches.length - 1];
+ if (lastMatch) {
+ const routeName = lastMatch.id;
getCurrentScope().setTransactionName(routeName);
const activeRootSpan = getActiveSpan();
diff --git a/packages/remix/src/index.client.tsx b/packages/remix/src/index.client.tsx
index 17bcf6c46b31..2ff8e1a222a5 100644
--- a/packages/remix/src/index.client.tsx
+++ b/packages/remix/src/index.client.tsx
@@ -1,11 +1,31 @@
import { applySdkMetadata, setTag } from '@sentry/core';
import { init as reactInit } from '@sentry/react';
+import { logger } from '@sentry/utils';
+import { DEBUG_BUILD } from './utils/debug-build';
import type { RemixOptions } from './utils/remixOptions';
export { captureRemixErrorBoundaryError } from './client/errors';
export { withSentry } from './client/performance';
export { browserTracingIntegration } from './client/browserTracingIntegration';
+// This is a no-op function that does nothing. It's here to make sure that the
+// function signature is the same as in the server SDK.
+// See issue: https://github.com/getsentry/sentry-javascript/issues/9594
+/* eslint-disable @typescript-eslint/no-unused-vars */
+export async function captureRemixServerException(
+ err: unknown,
+ name: string,
+ request: Request,
+ isRemixV2: boolean,
+): Promise {
+ DEBUG_BUILD &&
+ logger.warn(
+ '`captureRemixServerException` is a server-only function and should not be called in the browser. ' +
+ 'This function is a no-op in the browser environment.',
+ );
+}
+/* eslint-enable @typescript-eslint/no-unused-vars */
+
export * from '@sentry/react';
export function init(options: RemixOptions): void {
diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts
index a6476b692fbf..9ffc69a4ec12 100644
--- a/packages/remix/src/index.server.ts
+++ b/packages/remix/src/index.server.ts
@@ -1,10 +1,18 @@
-import { applySdkMetadata, isInitialized } from '@sentry/core';
+import { applySdkMetadata } from '@sentry/core';
import type { NodeOptions } from '@sentry/node';
-import { init as nodeInit, setTag } from '@sentry/node';
+import {
+ getDefaultIntegrations as getDefaultNodeIntegrations,
+ init as nodeInit,
+ isInitialized,
+ setTag,
+} from '@sentry/node';
+import type { Integration } from '@sentry/types';
import { logger } from '@sentry/utils';
import { DEBUG_BUILD } from './utils/debug-build';
import { instrumentServer } from './utils/instrumentServer';
+import { httpIntegration } from './utils/integrations/http';
+import { remixIntegration } from './utils/integrations/opentelemetry';
import type { RemixOptions } from './utils/remixOptions';
// We need to explicitly export @sentry/node as they end up under `default` in ESM builds
@@ -41,7 +49,6 @@ export {
withScope,
withIsolationScope,
makeNodeTransport,
- getDefaultIntegrations,
defaultStackParser,
lastEventId,
flush,
@@ -109,20 +116,48 @@ export {
export * from '@sentry/node';
export {
- captureRemixServerException,
// eslint-disable-next-line deprecation/deprecation
wrapRemixHandleError,
sentryHandleError,
wrapHandleErrorWithSentry,
} from './utils/instrumentServer';
+
+export { captureRemixServerException } from './utils/errors';
+
export { ErrorBoundary, withErrorBoundary } from '@sentry/react';
export { withSentry } from './client/performance';
export { captureRemixErrorBoundaryError } from './client/errors';
export { browserTracingIntegration } from './client/browserTracingIntegration';
-export { wrapExpressCreateRequestHandler } from './utils/serverAdapters/express';
export type { SentryMetaArgs } from './utils/types';
+/**
+ * Returns the default Remix integrations.
+ *
+ * @param options The options for the SDK.
+ */
+export function getRemixDefaultIntegrations(options: RemixOptions): Integration[] {
+ return [
+ ...getDefaultNodeIntegrations(options as NodeOptions).filter(integration => integration.name !== 'Http'),
+ httpIntegration(),
+ options.autoInstrumentRemix ? remixIntegration() : undefined,
+ ].filter(int => int) as Integration[];
+}
+
+/**
+ * Returns the given Express createRequestHandler function.
+ * This function is no-op and only returns the given function.
+ *
+ * @deprecated No need to wrap the Express request handler.
+ * @param createRequestHandlerFn The Remix Express `createRequestHandler`.
+ * @returns `createRequestHandler` function.
+ */
+export function wrapExpressCreateRequestHandler(createRequestHandlerFn: unknown): unknown {
+ DEBUG_BUILD && logger.warn('wrapExpressCreateRequestHandler is deprecated and no longer needed.');
+
+ return createRequestHandlerFn;
+}
+
/** Initializes Sentry Remix SDK on Node. */
export function init(options: RemixOptions): void {
applySdkMetadata(options, 'remix', ['remix', 'node']);
@@ -133,9 +168,11 @@ export function init(options: RemixOptions): void {
return;
}
- instrumentServer();
+ options.defaultIntegrations = getRemixDefaultIntegrations(options as NodeOptions);
nodeInit(options as NodeOptions);
+ instrumentServer(options);
+
setTag('runtime', 'node');
}
diff --git a/packages/remix/src/index.types.ts b/packages/remix/src/index.types.ts
index d573fd55bd9a..05bc6483218e 100644
--- a/packages/remix/src/index.types.ts
+++ b/packages/remix/src/index.types.ts
@@ -18,6 +18,13 @@ export declare const contextLinesIntegration: typeof clientSdk.contextLinesInteg
export declare const getDefaultIntegrations: (options: Options) => Integration[];
export declare const defaultStackParser: StackParser;
+export declare function captureRemixServerException(
+ err: unknown,
+ name: string,
+ request: Request,
+ isRemixV2: boolean,
+): Promise;
+
// This variable is not a runtime variable but just a type to tell typescript that the methods below can either come
// from the client SDK or from the server SDK. TypeScript is smart enough to understand that these resolve to the same
// methods from `@sentry/core`.
diff --git a/packages/remix/src/utils/errors.ts b/packages/remix/src/utils/errors.ts
new file mode 100644
index 000000000000..3c8943d2c107
--- /dev/null
+++ b/packages/remix/src/utils/errors.ts
@@ -0,0 +1,195 @@
+import type { AppData, DataFunctionArgs, EntryContext, HandleDocumentRequestFunction } from '@remix-run/node';
+import {
+ captureException,
+ getActiveSpan,
+ getClient,
+ getRootSpan,
+ handleCallbackErrors,
+ spanToJSON,
+} from '@sentry/core';
+import type { Span } from '@sentry/types';
+import { addExceptionMechanism, isPrimitive, logger, objectify } from '@sentry/utils';
+import { DEBUG_BUILD } from './debug-build';
+import type { RemixOptions } from './remixOptions';
+import { storeFormDataKeys } from './utils';
+import { extractData, isResponse, isRouteErrorResponse } from './vendor/response';
+import type { DataFunction, RemixRequest } from './vendor/types';
+import { normalizeRemixRequest } from './web-fetch';
+
+/**
+ * Captures an exception happened in the Remix server.
+ *
+ * @param err The error to capture.
+ * @param name The name of the origin function.
+ * @param request The request object.
+ * @param isRemixV2 Whether the error is from Remix v2 or not. Default is `true`.
+ *
+ * @returns A promise that resolves when the exception is captured.
+ */
+export async function captureRemixServerException(
+ err: unknown,
+ name: string,
+ request: Request,
+ isRemixV2: boolean = true,
+): Promise {
+ // Skip capturing if the thrown error is not a 5xx response
+ // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders
+ if (isRemixV2 && isRouteErrorResponse(err) && err.status < 500) {
+ return;
+ }
+
+ if (isResponse(err) && err.status < 500) {
+ return;
+ }
+ // Skip capturing if the request is aborted as Remix docs suggest
+ // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
+ if (request.signal.aborted) {
+ DEBUG_BUILD && logger.warn('Skipping capture of aborted request');
+ return;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let normalizedRequest: Record = request as unknown as any;
+
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ normalizedRequest = normalizeRemixRequest(request as unknown as any);
+ } catch (e) {
+ DEBUG_BUILD && logger.warn('Failed to normalize Remix request');
+ }
+
+ const objectifiedErr = objectify(err);
+
+ captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => {
+ const activeSpan = getActiveSpan();
+ const rootSpan = activeSpan && getRootSpan(activeSpan);
+ const activeRootSpanName = rootSpan ? spanToJSON(rootSpan).description : undefined;
+
+ scope.setSDKProcessingMetadata({
+ request: {
+ ...normalizedRequest,
+ // When `route` is not defined, `RequestData` integration uses the full URL
+ route: activeRootSpanName
+ ? {
+ path: activeRootSpanName,
+ }
+ : undefined,
+ },
+ });
+
+ scope.addEventProcessor(event => {
+ addExceptionMechanism(event, {
+ type: 'instrument',
+ handled: false,
+ data: {
+ function: name,
+ },
+ });
+
+ return event;
+ });
+
+ return scope;
+ });
+}
+
+/**
+ * Wraps the original `HandleDocumentRequestFunction` with error handling.
+ *
+ * @param origDocumentRequestFunction The original `HandleDocumentRequestFunction`.
+ * @param requestContext The request context.
+ * @param isRemixV2 Whether the Remix version is v2 or not.
+ *
+ * @returns The wrapped `HandleDocumentRequestFunction`.
+ */
+export function errorHandleDocumentRequestFunction(
+ this: unknown,
+ origDocumentRequestFunction: HandleDocumentRequestFunction,
+ requestContext: {
+ request: RemixRequest;
+ responseStatusCode: number;
+ responseHeaders: Headers;
+ context: EntryContext;
+ loadContext?: Record;
+ },
+ isRemixV2: boolean,
+): HandleDocumentRequestFunction {
+ const { request, responseStatusCode, responseHeaders, context, loadContext } = requestContext;
+
+ return handleCallbackErrors(
+ () => {
+ return origDocumentRequestFunction.call(this, request, responseStatusCode, responseHeaders, context, loadContext);
+ },
+ err => {
+ // This exists to capture the server-side rendering errors on Remix v1
+ // On Remix v2, we capture SSR errors at `handleError`
+ // We also skip primitives here, as we can't dedupe them, and also we don't expect any primitive SSR errors.
+ if (!isRemixV2 && !isPrimitive(err)) {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ captureRemixServerException(err, 'documentRequest', request, isRemixV2);
+ }
+
+ throw err;
+ },
+ );
+}
+
+/**
+ * Wraps the original `DataFunction` with error handling.
+ * This function also stores the form data keys if the action is being called.
+ *
+ * @param origFn The original `DataFunction`.
+ * @param name The name of the function.
+ * @param args The arguments of the function.
+ * @param isRemixV2 Whether the Remix version is v2 or not.
+ * @param span The span to store the form data keys.
+ *
+ * @returns The wrapped `DataFunction`.
+ */
+export async function errorHandleDataFunction(
+ this: unknown,
+ origFn: DataFunction,
+ name: string,
+ args: DataFunctionArgs,
+ isRemixV2: boolean,
+ span?: Span,
+): Promise {
+ return handleCallbackErrors(
+ async () => {
+ if (name === 'action' && span) {
+ const options = getClient()?.getOptions() as RemixOptions;
+
+ if (options.sendDefaultPii && options.captureActionFormDataKeys) {
+ await storeFormDataKeys(args, span);
+ }
+ }
+
+ return origFn.call(this, args);
+ },
+ err => {
+ // On Remix v2, we capture all unexpected errors (except the `Route Error Response`s / Thrown Responses) in `handleError` function.
+ // This is both for consistency and also avoid duplicates such as primitives like `string` or `number` being captured twice.
+ // Remix v1 does not have a `handleError` function, so we capture all errors here.
+ if (isRemixV2 ? isResponse(err) : true) {
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ captureRemixServerException(err, name, args.request);
+ }
+
+ throw err;
+ },
+ );
+}
+
+async function extractResponseError(response: Response): Promise {
+ const responseData = await extractData(response);
+
+ if (typeof responseData === 'string' && responseData.length > 0) {
+ return new Error(responseData);
+ }
+
+ if (response.statusText) {
+ return new Error(response.statusText);
+ }
+
+ return responseData;
+}
diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts
index 86f596b61eb7..e83c14dfbbc4 100644
--- a/packages/remix/src/utils/instrumentServer.ts
+++ b/packages/remix/src/utils/instrumentServer.ts
@@ -3,12 +3,9 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
- captureException,
- continueTrace,
getActiveSpan,
getClient,
getRootSpan,
- handleCallbackErrors,
hasTracingEnabled,
setHttpStatus,
spanToJSON,
@@ -16,30 +13,17 @@ import {
startSpan,
withIsolationScope,
} from '@sentry/core';
-import { getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry';
-import type { Span, TransactionSource, WrappedFunction } from '@sentry/types';
-import {
- addExceptionMechanism,
- dynamicSamplingContextToSentryBaggageHeader,
- fill,
- isNodeEnv,
- isPrimitive,
- loadModule,
- logger,
- objectify,
-} from '@sentry/utils';
+import { continueTrace, getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry';
+import type { TransactionSource, WrappedFunction } from '@sentry/types';
+import type { Span } from '@sentry/types';
+import { dynamicSamplingContextToSentryBaggageHeader, fill, isNodeEnv, loadModule, logger } from '@sentry/utils';
import { DEBUG_BUILD } from './debug-build';
+import { captureRemixServerException, errorHandleDataFunction, errorHandleDocumentRequestFunction } from './errors';
import { getFutureFlagsServer, getRemixVersionFromBuild } from './futureFlags';
-import {
- extractData,
- getRequestMatch,
- isDeferredData,
- isResponse,
- isRouteErrorResponse,
- json,
- matchServerRoutes,
-} from './vendor/response';
+import type { RemixOptions } from './remixOptions';
+import { createRoutes, getTransactionName } from './utils';
+import { extractData, isDeferredData, isResponse, isRouteErrorResponse, json } from './vendor/response';
import type {
AppData,
AppLoadContext,
@@ -58,7 +42,6 @@ import type {
import { normalizeRemixRequest } from './web-fetch';
let FUTURE_FLAGS: FutureConfig | undefined;
-let IS_REMIX_V2: boolean | undefined;
const redirectStatusCodes = new Set([301, 302, 303, 307, 308]);
function isRedirectResponse(response: Response): boolean {
@@ -69,20 +52,6 @@ function isCatchResponse(response: Response): boolean {
return response.headers.get('X-Remix-Catch') != null;
}
-async function extractResponseError(response: Response): Promise {
- const responseData = await extractData(response);
-
- if (typeof responseData === 'string' && responseData.length > 0) {
- return new Error(responseData);
- }
-
- if (response.statusText) {
- return new Error(response.statusText);
- }
-
- return responseData;
-}
-
/**
* Sentry utility to be used in place of `handleError` function of Remix v2
* Remix Docs: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
@@ -129,78 +98,7 @@ export function wrapHandleErrorWithSentry(
};
}
-/**
- * Captures an exception happened in the Remix server.
- *
- * @param err The error to capture.
- * @param name The name of the origin function.
- * @param request The request object.
- *
- * @returns A promise that resolves when the exception is captured.
- */
-export async function captureRemixServerException(err: unknown, name: string, request: Request): Promise {
- // Skip capturing if the thrown error is not a 5xx response
- // https://remix.run/docs/en/v1/api/conventions#throwing-responses-in-loaders
- if (IS_REMIX_V2 && isRouteErrorResponse(err) && err.status < 500) {
- return;
- }
-
- if (isResponse(err) && err.status < 500) {
- return;
- }
- // Skip capturing if the request is aborted as Remix docs suggest
- // Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
- if (request.signal.aborted) {
- DEBUG_BUILD && logger.warn('Skipping capture of aborted request');
- return;
- }
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- let normalizedRequest: Record = request as unknown as any;
-
- try {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- normalizedRequest = normalizeRemixRequest(request as unknown as any);
- } catch (e) {
- DEBUG_BUILD && logger.warn('Failed to normalize Remix request');
- }
-
- const objectifiedErr = objectify(err);
-
- captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => {
- const activeSpan = getActiveSpan();
- const rootSpan = activeSpan && getRootSpan(activeSpan);
- const activeRootSpanName = rootSpan ? spanToJSON(rootSpan).description : undefined;
-
- scope.setSDKProcessingMetadata({
- request: {
- ...normalizedRequest,
- // When `route` is not defined, `RequestData` integration uses the full URL
- route: activeRootSpanName
- ? {
- path: activeRootSpanName,
- }
- : undefined,
- },
- });
-
- scope.addEventProcessor(event => {
- addExceptionMechanism(event, {
- type: 'instrument',
- handled: false,
- data: {
- function: name,
- },
- });
-
- return event;
- });
-
- return scope;
- });
-}
-
-function makeWrappedDocumentRequestFunction(remixVersion?: number) {
+function makeWrappedDocumentRequestFunction(autoInstrumentRemix?: boolean, remixVersion?: number) {
return function (origDocumentRequestFunction: HandleDocumentRequestFunction): HandleDocumentRequestFunction {
return async function (
this: unknown,
@@ -210,52 +108,52 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) {
context: EntryContext,
loadContext?: Record,
): Promise {
- const activeSpan = getActiveSpan();
- const rootSpan = activeSpan && getRootSpan(activeSpan);
-
- const name = rootSpan ? spanToJSON(rootSpan).description : undefined;
+ const documentRequestContext = {
+ request,
+ responseStatusCode,
+ responseHeaders,
+ context,
+ loadContext,
+ };
- return startSpan(
- {
- // If we don't have a root span, `onlyIfParent` will lead to the span not being created anyhow
- // So we don't need to care too much about the fallback name, it's just for typing purposes....
- name: name || '',
- onlyIfParent: true,
- attributes: {
- method: request.method,
- url: request.url,
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.remix',
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.remix.document_request',
- },
- },
- () => {
- return handleCallbackErrors(
- () => {
- return origDocumentRequestFunction.call(
- this,
- request,
- responseStatusCode,
- responseHeaders,
- context,
- loadContext,
- );
+ const isRemixV2 = FUTURE_FLAGS?.v2_errorBoundary || remixVersion === 2;
+
+ if (!autoInstrumentRemix) {
+ const activeSpan = getActiveSpan();
+ const rootSpan = activeSpan && getRootSpan(activeSpan);
+
+ const name = rootSpan ? spanToJSON(rootSpan).description : undefined;
+
+ return startSpan(
+ {
+ // If we don't have a root span, `onlyIfParent` will lead to the span not being created anyhow
+ // So we don't need to care too much about the fallback name, it's just for typing purposes....
+ name: name || '',
+ onlyIfParent: true,
+ attributes: {
+ method: request.method,
+ url: request.url,
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.remix',
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.remix.document_request',
},
- err => {
- const isRemixV1 = !FUTURE_FLAGS?.v2_errorBoundary && remixVersion !== 2;
-
- // This exists to capture the server-side rendering errors on Remix v1
- // On Remix v2, we capture SSR errors at `handleError`
- // We also skip primitives here, as we can't dedupe them, and also we don't expect any primitive SSR errors.
- if (isRemixV1 && !isPrimitive(err)) {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- captureRemixServerException(err, 'documentRequest', request);
- }
-
- throw err;
- },
- );
- },
- );
+ },
+ () => {
+ return errorHandleDocumentRequestFunction.call(
+ this,
+ origDocumentRequestFunction,
+ documentRequestContext,
+ isRemixV2,
+ );
+ },
+ );
+ } else {
+ return errorHandleDocumentRequestFunction.call(
+ this,
+ origDocumentRequestFunction,
+ documentRequestContext,
+ isRemixV2,
+ );
+ }
};
};
}
@@ -265,82 +163,41 @@ function makeWrappedDataFunction(
id: string,
name: 'action' | 'loader',
remixVersion: number,
- manuallyInstrumented: boolean,
+ autoInstrumentRemix?: boolean,
): DataFunction {
return async function (this: unknown, args: DataFunctionArgs): Promise {
- if (args.context.__sentry_express_wrapped__ && !manuallyInstrumented) {
- return origFn.call(this, args);
- }
+ const isRemixV2 = FUTURE_FLAGS?.v2_errorBoundary || remixVersion === 2;
- return startSpan(
- {
- op: `function.remix.${name}`,
- name: id,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.remix',
- name,
- },
- },
- span => {
- return handleCallbackErrors(
- async () => {
- if (span) {
- const options = getClient()?.getOptions();
-
- // We only capture form data for `action` functions, when `sendDefaultPii` is enabled.
- if (name === 'action' && options?.sendDefaultPii) {
- try {
- // We clone the request for Remix be able to read the FormData later.
- const clonedRequest = args.request.clone();
-
- // This only will return the last name of multiple file uploads in a single FormData entry.
- // We can switch to `unstable_parseMultipartFormData` when it's stable.
- // https://remix.run/docs/en/main/utils/parse-multipart-form-data#unstable_parsemultipartformdata
- const formData = await clonedRequest.formData();
-
- formData.forEach((value, key) => {
- span.setAttribute(
- `remix.action_form_data.${key}`,
- typeof value === 'string' ? value : '[non-string value]',
- );
- });
- } catch (e) {
- DEBUG_BUILD && logger.warn('Failed to read FormData from request', e);
- }
- }
- }
-
- return origFn.call(this, args);
- },
- err => {
- const isRemixV2 = FUTURE_FLAGS?.v2_errorBoundary || remixVersion === 2;
-
- // On Remix v2, we capture all unexpected errors (except the `Route Error Response`s / Thrown Responses) in `handleError` function.
- // This is both for consistency and also avoid duplicates such as primitives like `string` or `number` being captured twice.
- // Remix v1 does not have a `handleError` function, so we capture all errors here.
- if (isRemixV2 ? isResponse(err) : true) {
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
- captureRemixServerException(err, name, args.request);
- }
-
- throw err;
+ if (!autoInstrumentRemix) {
+ return startSpan(
+ {
+ op: `function.remix.${name}`,
+ name: id,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.remix',
+ name,
},
- );
- },
- );
+ },
+ (span: Span) => {
+ return errorHandleDataFunction.call(this, origFn, name, args, isRemixV2, span);
+ },
+ );
+ } else {
+ return errorHandleDataFunction.call(this, origFn, name, args, isRemixV2);
+ }
};
}
const makeWrappedAction =
- (id: string, remixVersion: number, manuallyInstrumented: boolean) =>
+ (id: string, remixVersion: number, autoInstrumentRemix?: boolean) =>
(origAction: DataFunction): DataFunction => {
- return makeWrappedDataFunction(origAction, id, 'action', remixVersion, manuallyInstrumented);
+ return makeWrappedDataFunction(origAction, id, 'action', remixVersion, autoInstrumentRemix);
};
const makeWrappedLoader =
- (id: string, remixVersion: number, manuallyInstrumented: boolean) =>
+ (id: string, remixVersion: number, autoInstrumentRemix?: boolean) =>
(origLoader: DataFunction): DataFunction => {
- return makeWrappedDataFunction(origLoader, id, 'loader', remixVersion, manuallyInstrumented);
+ return makeWrappedDataFunction(origLoader, id, 'loader', remixVersion, autoInstrumentRemix);
};
function getTraceAndBaggage(): {
@@ -409,88 +266,33 @@ function makeWrappedRootLoader(remixVersion: number) {
};
}
-/**
- * Creates routes from the server route manifest
- *
- * @param manifest
- * @param parentId
- */
-export function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] {
- return Object.entries(manifest)
- .filter(([, route]) => route.parentId === parentId)
- .map(([id, route]) => ({
- ...route,
- children: createRoutes(manifest, id),
- }));
-}
-
-/**
- * Starts a new active span for the given request to be used by different `RequestHandler` wrappers.
- */
-export function startRequestHandlerSpan(
- {
- name,
- source,
- sentryTrace,
- baggage,
- method,
- }: {
- name: string;
- source: TransactionSource;
- sentryTrace: string;
- baggage: string;
- method: string;
- },
- callback: (span: Span) => T,
-): T {
- return continueTrace(
- {
- sentryTrace,
- baggage,
- },
- () => {
- return startSpan(
- {
- name,
- attributes: {
- [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix',
- [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
- [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
- method,
- },
- },
- callback,
- );
- },
- );
-}
-
-/**
- * Get transaction name from routes and url
- */
-export function getTransactionName(routes: ServerRoute[], url: URL): [string, TransactionSource] {
- const matches = matchServerRoutes(routes, url.pathname);
- const match = matches && getRequestMatch(url, matches);
- return match === null ? [url.pathname, 'url'] : [match.route.id || 'no-route-id', 'route'];
-}
-
-function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBuild): RequestHandler {
- const routes = createRoutes(build.routes);
+function wrapRequestHandler(
+ origRequestHandler: RequestHandler,
+ build: ServerBuild | (() => ServerBuild | Promise),
+ autoInstrumentRemix: boolean,
+): RequestHandler {
+ let resolvedBuild: ServerBuild;
+ let routes: ServerRoute[];
+ let name: string;
+ let source: TransactionSource;
return async function (this: unknown, request: RemixRequest, loadContext?: AppLoadContext): Promise {
- // This means that the request handler of the adapter (ex: express) is already wrapped.
- // So we don't want to double wrap it.
- if (loadContext?.__sentry_express_wrapped__) {
- return origRequestHandler.call(this, request, loadContext);
- }
-
const upperCaseMethod = request.method.toUpperCase();
-
// We don't want to wrap OPTIONS and HEAD requests
if (upperCaseMethod === 'OPTIONS' || upperCaseMethod === 'HEAD') {
return origRequestHandler.call(this, request, loadContext);
}
+ if (!autoInstrumentRemix) {
+ if (typeof build === 'function') {
+ resolvedBuild = await build();
+ } else {
+ resolvedBuild = build;
+ }
+
+ routes = createRoutes(resolvedBuild.routes);
+ }
+
return withIsolationScope(async isolationScope => {
const options = getClient()?.getOptions();
@@ -502,10 +304,13 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui
DEBUG_BUILD && logger.warn('Failed to normalize Remix request');
}
- const url = new URL(request.url);
- const [name, source] = getTransactionName(routes, url);
+ if (!autoInstrumentRemix) {
+ const url = new URL(request.url);
+ [name, source] = getTransactionName(routes, url);
+
+ isolationScope.setTransactionName(name);
+ }
- isolationScope.setTransactionName(name);
isolationScope.setSDKProcessingMetadata({
request: {
...normalizedRequest,
@@ -519,37 +324,45 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui
return origRequestHandler.call(this, request, loadContext);
}
- return startRequestHandlerSpan(
+ return continueTrace(
{
- name,
- source,
sentryTrace: request.headers.get('sentry-trace') || '',
baggage: request.headers.get('baggage') || '',
- method: request.method,
},
- async span => {
- const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
+ async () => {
+ if (!autoInstrumentRemix) {
+ return startSpan(
+ {
+ name,
+ attributes: {
+ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix',
+ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
+ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
+ method: request.method,
+ },
+ },
+ async span => {
+ const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
- if (isResponse(res)) {
- setHttpStatus(span, res.status);
+ if (isResponse(res)) {
+ setHttpStatus(span, res.status);
+ }
+
+ return res;
+ },
+ );
}
- return res;
+ return (await origRequestHandler.call(this, request, loadContext)) as Response;
},
);
});
};
}
-/**
- * Instruments `remix` ServerBuild for performance tracing and error tracking.
- */
-export function instrumentBuild(build: ServerBuild, manuallyInstrumented: boolean = false): ServerBuild {
+function instrumentBuildCallback(build: ServerBuild, autoInstrumentRemix: boolean): ServerBuild {
const routes: ServerRouteManifest = {};
-
const remixVersion = getRemixVersionFromBuild(build);
- IS_REMIX_V2 = remixVersion === 2;
-
const wrappedEntry = { ...build.entry, module: { ...build.entry.module } };
// Not keeping boolean flags like it's done for `requestHandler` functions,
@@ -558,7 +371,7 @@ export function instrumentBuild(build: ServerBuild, manuallyInstrumented: boolea
// We should be able to wrap them, as they may not be wrapped before.
const defaultExport = wrappedEntry.module.default as undefined | WrappedFunction;
if (defaultExport && !defaultExport.__sentry_original__) {
- fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction(remixVersion));
+ fill(wrappedEntry.module, 'default', makeWrappedDocumentRequestFunction(autoInstrumentRemix, remixVersion));
}
for (const [id, route] of Object.entries(build.routes)) {
@@ -566,12 +379,12 @@ export function instrumentBuild(build: ServerBuild, manuallyInstrumented: boolea
const routeAction = wrappedRoute.module.action as undefined | WrappedFunction;
if (routeAction && !routeAction.__sentry_original__) {
- fill(wrappedRoute.module, 'action', makeWrappedAction(id, remixVersion, manuallyInstrumented));
+ fill(wrappedRoute.module, 'action', makeWrappedAction(id, remixVersion, autoInstrumentRemix));
}
const routeLoader = wrappedRoute.module.loader as undefined | WrappedFunction;
if (routeLoader && !routeLoader.__sentry_original__) {
- fill(wrappedRoute.module, 'loader', makeWrappedLoader(id, remixVersion, manuallyInstrumented));
+ fill(wrappedRoute.module, 'loader', makeWrappedLoader(id, remixVersion, autoInstrumentRemix));
}
// Entry module should have a loader function to provide `sentry-trace` and `baggage`
@@ -591,23 +404,57 @@ export function instrumentBuild(build: ServerBuild, manuallyInstrumented: boolea
return { ...build, routes, entry: wrappedEntry };
}
-function makeWrappedCreateRequestHandler(
- origCreateRequestHandler: CreateRequestHandlerFunction,
-): CreateRequestHandlerFunction {
- return function (this: unknown, build: ServerBuild, ...args: unknown[]): RequestHandler {
+/**
+ * Instruments `remix` ServerBuild for performance tracing and error tracking.
+ */
+export function instrumentBuild(
+ build: ServerBuild | (() => ServerBuild | Promise),
+ options: RemixOptions,
+): ServerBuild | (() => ServerBuild | Promise) {
+ const autoInstrumentRemix = options?.autoInstrumentRemix || false;
+
+ if (typeof build === 'function') {
+ return function () {
+ const resolvedBuild = build();
+
+ if (resolvedBuild instanceof Promise) {
+ return resolvedBuild.then(build => {
+ FUTURE_FLAGS = getFutureFlagsServer(build);
+
+ return instrumentBuildCallback(build, autoInstrumentRemix);
+ });
+ } else {
+ FUTURE_FLAGS = getFutureFlagsServer(resolvedBuild);
+
+ return instrumentBuildCallback(resolvedBuild, autoInstrumentRemix);
+ }
+ };
+ } else {
FUTURE_FLAGS = getFutureFlagsServer(build);
- const newBuild = instrumentBuild(build, false);
- const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args);
- return wrapRequestHandler(requestHandler, newBuild);
- };
+ return instrumentBuildCallback(build, autoInstrumentRemix);
+ }
}
+const makeWrappedCreateRequestHandler = (options: RemixOptions) =>
+ function (origCreateRequestHandler: CreateRequestHandlerFunction): CreateRequestHandlerFunction {
+ return function (
+ this: unknown,
+ build: ServerBuild | (() => Promise),
+ ...args: unknown[]
+ ): RequestHandler {
+ const newBuild = instrumentBuild(build, options);
+ const requestHandler = origCreateRequestHandler.call(this, newBuild, ...args);
+
+ return wrapRequestHandler(requestHandler, newBuild, options.autoInstrumentRemix || false);
+ };
+ };
+
/**
* Monkey-patch Remix's `createRequestHandler` from `@remix-run/server-runtime`
* which Remix Adapters (https://remix.run/docs/en/v1/api/remix) use underneath.
*/
-export function instrumentServer(): void {
+export function instrumentServer(options: RemixOptions): void {
const pkg = loadModule<{
createRequestHandler: CreateRequestHandlerFunction;
}>('@remix-run/server-runtime');
@@ -618,5 +465,5 @@ export function instrumentServer(): void {
return;
}
- fill(pkg, 'createRequestHandler', makeWrappedCreateRequestHandler);
+ fill(pkg, 'createRequestHandler', makeWrappedCreateRequestHandler(options));
}
diff --git a/packages/remix/src/utils/integrations/http.ts b/packages/remix/src/utils/integrations/http.ts
new file mode 100644
index 000000000000..7c4b80f44fe7
--- /dev/null
+++ b/packages/remix/src/utils/integrations/http.ts
@@ -0,0 +1,42 @@
+// This integration is ported from the Next.JS SDK.
+import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
+import { httpIntegration as originalHttpIntegration } from '@sentry/node';
+import type { IntegrationFn } from '@sentry/types';
+
+class RemixHttpIntegration extends HttpInstrumentation {
+ // Instead of the default behavior, we just don't do any wrapping for incoming requests
+ protected _getPatchIncomingRequestFunction(_component: 'http' | 'https') {
+ return (
+ original: (event: string, ...args: unknown[]) => boolean,
+ ): ((this: unknown, event: string, ...args: unknown[]) => boolean) => {
+ return function incomingRequest(this: unknown, event: string, ...args: unknown[]): boolean {
+ return original.apply(this, [event, ...args]);
+ };
+ };
+ }
+}
+
+interface HttpOptions {
+ /**
+ * Whether breadcrumbs should be recorded for requests.
+ * Defaults to true
+ */
+ breadcrumbs?: boolean;
+
+ /**
+ * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`.
+ * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled.
+ */
+ ignoreOutgoingRequests?: (url: string) => boolean;
+}
+
+/**
+ * The http integration instruments Node's internal http and https modules.
+ * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span.
+ */
+export const httpIntegration = ((options: HttpOptions = {}) => {
+ return originalHttpIntegration({
+ ...options,
+ _instrumentation: RemixHttpIntegration,
+ });
+}) satisfies IntegrationFn;
diff --git a/packages/remix/src/utils/integrations/opentelemetry.ts b/packages/remix/src/utils/integrations/opentelemetry.ts
new file mode 100644
index 000000000000..24648bb8db22
--- /dev/null
+++ b/packages/remix/src/utils/integrations/opentelemetry.ts
@@ -0,0 +1,62 @@
+import { RemixInstrumentation } from 'opentelemetry-instrumentation-remix';
+
+import { defineIntegration } from '@sentry/core';
+import { SEMANTIC_ATTRIBUTE_SENTRY_OP, generateInstrumentOnce, getClient, spanToJSON } from '@sentry/node';
+import type { Client, IntegrationFn, Span } from '@sentry/types';
+import type { RemixOptions } from '../remixOptions';
+
+const INTEGRATION_NAME = 'Remix';
+
+const instrumentRemix = generateInstrumentOnce(
+ INTEGRATION_NAME,
+ (_options?: RemixOptions) =>
+ new RemixInstrumentation({
+ actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
+ }),
+);
+
+const _remixIntegration = (() => {
+ return {
+ name: 'Remix',
+ setupOnce() {
+ const client = getClient();
+ const options = client?.getOptions();
+
+ instrumentRemix(options);
+ },
+
+ setup(client: Client) {
+ client.on('spanStart', span => {
+ addRemixSpanAttributes(span);
+ });
+ },
+ };
+}) satisfies IntegrationFn;
+
+const addRemixSpanAttributes = (span: Span): void => {
+ const attributes = spanToJSON(span).data || {};
+
+ // this is one of: loader, action, requestHandler
+ const type = attributes['code.function'];
+
+ // If this is already set, or we have no remix span, no need to process again...
+ if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] || !type) {
+ return;
+ }
+
+ // `requestHandler` span from `opentelemetry-instrumentation-remix` is the main server span.
+ // It should be marked as the `http.server` operation.
+ // The incoming requests are skipped by the custom `RemixHttpIntegration` package.
+ if (type === 'requestHandler') {
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
+ return;
+ }
+
+ // All other spans are marked as `remix` operations with their specific type [loader, action]
+ span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.remix`);
+};
+
+/**
+ * Instrumentation for aws-sdk package
+ */
+export const remixIntegration = defineIntegration(_remixIntegration);
diff --git a/packages/remix/src/utils/remixOptions.ts b/packages/remix/src/utils/remixOptions.ts
index 4a1fe13e18e1..4f73eca92ff3 100644
--- a/packages/remix/src/utils/remixOptions.ts
+++ b/packages/remix/src/utils/remixOptions.ts
@@ -2,4 +2,7 @@ import type { NodeOptions } from '@sentry/node';
import type { BrowserOptions } from '@sentry/react';
import type { Options } from '@sentry/types';
-export type RemixOptions = Options | BrowserOptions | NodeOptions;
+export type RemixOptions = (Options | BrowserOptions | NodeOptions) & {
+ captureActionFormDataKeys?: Record;
+ autoInstrumentRemix?: boolean;
+};
diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts
deleted file mode 100644
index d4caed091015..000000000000
--- a/packages/remix/src/utils/serverAdapters/express.ts
+++ /dev/null
@@ -1,226 +0,0 @@
-import { getClient, hasTracingEnabled, setHttpStatus, withIsolationScope } from '@sentry/core';
-import { flush } from '@sentry/node';
-import type { Span, TransactionSource } from '@sentry/types';
-import { extractRequestData, fill, isString, logger } from '@sentry/utils';
-
-import { DEBUG_BUILD } from '../debug-build';
-import { createRoutes, getTransactionName, instrumentBuild, startRequestHandlerSpan } from '../instrumentServer';
-import type {
- AppLoadContext,
- ExpressCreateRequestHandler,
- ExpressCreateRequestHandlerOptions,
- ExpressNextFunction,
- ExpressRequest,
- ExpressRequestHandler,
- ExpressResponse,
- GetLoadContextFunction,
- ServerBuild,
- ServerRoute,
-} from '../vendor/types';
-
-function wrapExpressRequestHandler(
- origRequestHandler: ExpressRequestHandler,
- build: ServerBuild | (() => Promise | ServerBuild),
-): ExpressRequestHandler {
- let routes: ServerRoute[];
-
- return async function (
- this: unknown,
- req: ExpressRequest,
- res: ExpressResponse,
- next: ExpressNextFunction,
- ): Promise {
- await withIsolationScope(async isolationScope => {
- // eslint-disable-next-line @typescript-eslint/unbound-method
- res.end = wrapEndMethod(res.end);
-
- const request = extractRequestData(req);
- const options = getClient()?.getOptions();
-
- isolationScope.setSDKProcessingMetadata({ request });
-
- if (!options || !hasTracingEnabled(options) || !request.url || !request.method) {
- return origRequestHandler.call(this, req, res, next);
- }
-
- const url = new URL(request.url);
-
- // This is only meant to be used on development servers, so we don't need to worry about performance here
- if (build && typeof build === 'function') {
- const resolvedBuild = build();
-
- if (resolvedBuild instanceof Promise) {
- return resolvedBuild.then(resolved => {
- routes = createRoutes(resolved.routes);
-
- const [name, source] = getTransactionName(routes, url);
- isolationScope.setTransactionName(name);
-
- startRequestHandlerTransaction.call(this, origRequestHandler, req, res, next, name, source);
- });
- } else {
- routes = createRoutes(resolvedBuild.routes);
- }
- } else {
- routes = createRoutes(build.routes);
- }
-
- const [name, source] = getTransactionName(routes, url);
- isolationScope.setTransactionName(name);
-
- return startRequestHandlerTransaction.call(this, origRequestHandler, req, res, next, name, source);
- });
- };
-}
-
-function startRequestHandlerTransaction(
- this: unknown,
- origRequestHandler: ExpressRequestHandler,
- req: ExpressRequest,
- res: ExpressResponse,
- next: ExpressNextFunction,
- name: string,
- source: TransactionSource,
-): unknown {
- return startRequestHandlerSpan(
- {
- name,
- source,
- sentryTrace: (req.headers && isString(req.headers['sentry-trace']) && req.headers['sentry-trace']) || '',
- baggage: (req.headers && isString(req.headers.baggage) && req.headers.baggage) || '',
- method: req.method,
- },
- span => {
- // save a link to the transaction on the response, so that even if there's an error (landing us outside of
- // the domain), we can still finish it (albeit possibly missing some scope data)
- (res as AugmentedExpressResponse).__sentrySpan = span;
- return origRequestHandler.call(this, req, res, next);
- },
- );
-}
-
-function wrapGetLoadContext(origGetLoadContext: () => AppLoadContext): GetLoadContextFunction {
- return function (this: unknown, req: ExpressRequest, res: ExpressResponse): AppLoadContext {
- const loadContext = (origGetLoadContext.call(this, req, res) || {}) as AppLoadContext;
-
- loadContext['__sentry_express_wrapped__'] = true;
-
- return loadContext;
- };
-}
-
-// wrap build function which returns either a Promise or the build itself
-// This is currently only required for Vite development mode with HMR
-function wrapBuildFn(origBuildFn: () => Promise | ServerBuild): () => Promise | ServerBuild {
- return async function (this: unknown, ...args: unknown[]) {
- const resolvedBuild = origBuildFn.call(this, ...args);
-
- if (resolvedBuild instanceof Promise) {
- return resolvedBuild.then(resolved => {
- return instrumentBuild(resolved, true);
- });
- }
-
- return instrumentBuild(resolvedBuild, true);
- };
-}
-
-// A wrapper around build if it's a Promise or a function that returns a Promise that calls instrumentServer on the resolved value
-// This is currently only required for Vite development mode with HMR
-function wrapBuild(
- build: ServerBuild | (() => Promise | ServerBuild),
-): ServerBuild | (() => Promise | ServerBuild) {
- if (typeof build === 'function') {
- return wrapBuildFn(build);
- }
-
- return instrumentBuild(build, true);
-}
-
-/**
- * Instruments `createRequestHandler` from `@remix-run/express`
- */
-export function wrapExpressCreateRequestHandler(
- origCreateRequestHandler: ExpressCreateRequestHandler,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
-): (options: any) => ExpressRequestHandler {
- return function (this: unknown, options: ExpressCreateRequestHandlerOptions): ExpressRequestHandler {
- if (!('getLoadContext' in options)) {
- options['getLoadContext'] = () => ({});
- }
-
- fill(options, 'getLoadContext', wrapGetLoadContext);
-
- const newBuild = wrapBuild(options.build);
- const requestHandler = origCreateRequestHandler.call(this, {
- ...options,
- build: newBuild,
- });
-
- return wrapExpressRequestHandler(requestHandler, newBuild);
- };
-}
-
-export type AugmentedExpressResponse = ExpressResponse & {
- __sentrySpan?: Span;
-};
-
-type ResponseEndMethod = AugmentedExpressResponse['end'];
-type WrappedResponseEndMethod = AugmentedExpressResponse['end'];
-
-/**
- * Wrap `res.end()` so that it closes the transaction and flushes events before letting the request finish.
- *
- * Note: This wraps a sync method with an async method. While in general that's not a great idea in terms of keeping
- * things in the right order, in this case it's safe, because the native `.end()` actually *is* async, and its run
- * actually *is* awaited, just manually so (which reflects the fact that the core of the request/response code in Node
- * by far predates the introduction of `async`/`await`). When `.end()` is done, it emits the `prefinish` event, and
- * only once that fires does request processing continue. See
- * https://github.com/nodejs/node/commit/7c9b607048f13741173d397795bac37707405ba7.
- *
- * @param origEnd The original `res.end()` method
- * @returns The wrapped version
- */
-function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
- return async function newEnd(this: AugmentedExpressResponse, ...args: unknown[]) {
- await finishSentryProcessing(this);
-
- return origEnd.call(this, ...args);
- } as unknown as WrappedResponseEndMethod;
-}
-
-/**
- * Close the open transaction (if any) and flush events to Sentry.
- *
- * @param res The outgoing response for this request, on which the transaction is stored
- */
-async function finishSentryProcessing(res: AugmentedExpressResponse): Promise {
- const { __sentrySpan: span } = res;
-
- if (span) {
- setHttpStatus(span, res.statusCode);
-
- // Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
- // transaction closes, and make sure to wait until that's done before flushing events
- await new Promise(resolve => {
- setImmediate(() => {
- // Double checking whether the span is not already finished,
- // OpenTelemetry gives error if we try to end a finished span
- if (span.isRecording()) {
- span.end();
- }
- resolve();
- });
- });
- }
-
- // Flush the event queue to ensure that events get sent to Sentry before the response is finished and the lambda
- // ends. If there was an error, rethrow it so that the normal exception-handling mechanisms can apply.
- try {
- DEBUG_BUILD && logger.log('Flushing events...');
- await flush(2000);
- DEBUG_BUILD && logger.log('Done flushing events');
- } catch (e) {
- DEBUG_BUILD && logger.log('Error while flushing events:\n', e);
- }
-}
diff --git a/packages/remix/src/utils/utils.ts b/packages/remix/src/utils/utils.ts
new file mode 100644
index 000000000000..fed9e721e013
--- /dev/null
+++ b/packages/remix/src/utils/utils.ts
@@ -0,0 +1,51 @@
+import type { DataFunctionArgs } from '@remix-run/node';
+import type { Span, TransactionSource } from '@sentry/types';
+import { logger } from '@sentry/utils';
+import { DEBUG_BUILD } from './debug-build';
+import { getRequestMatch, matchServerRoutes } from './vendor/response';
+import type { ServerRoute, ServerRouteManifest } from './vendor/types';
+
+/**
+ *
+ */
+export async function storeFormDataKeys(args: DataFunctionArgs, span: Span): Promise {
+ try {
+ // We clone the request for Remix be able to read the FormData later.
+ const clonedRequest = args.request.clone();
+
+ // This only will return the last name of multiple file uploads in a single FormData entry.
+ // We can switch to `unstable_parseMultipartFormData` when it's stable.
+ // https://remix.run/docs/en/main/utils/parse-multipart-form-data#unstable_parsemultipartformdata
+ const formData = await clonedRequest.formData();
+
+ formData.forEach((value, key) => {
+ span.setAttribute(`remix.action_form_data.${key}`, typeof value === 'string' ? value : '[non-string value]');
+ });
+ } catch (e) {
+ DEBUG_BUILD && logger.warn('Failed to read FormData from request', e);
+ }
+}
+
+/**
+ * Get transaction name from routes and url
+ */
+export function getTransactionName(routes: ServerRoute[], url: URL): [string, TransactionSource] {
+ const matches = matchServerRoutes(routes, url.pathname);
+ const match = matches && getRequestMatch(url, matches);
+ return match === null ? [url.pathname, 'url'] : [match.route.id || 'no-route-id', 'route'];
+}
+
+/**
+ * Creates routes from the server route manifest
+ *
+ * @param manifest
+ * @param parentId
+ */
+export function createRoutes(manifest: ServerRouteManifest, parentId?: string): ServerRoute[] {
+ return Object.entries(manifest)
+ .filter(([, route]) => route.parentId === parentId)
+ .map(([id, route]) => ({
+ ...route,
+ children: createRoutes(manifest, id),
+ }));
+}
diff --git a/packages/remix/src/utils/vendor/response.ts b/packages/remix/src/utils/vendor/response.ts
index 8a8421ab06fd..c79b253adc7e 100644
--- a/packages/remix/src/utils/vendor/response.ts
+++ b/packages/remix/src/utils/vendor/response.ts
@@ -118,10 +118,10 @@ export function getRequestMatch(
url: URL,
matches: AgnosticRouteMatch[],
): AgnosticRouteMatch {
- const match = matches.slice(-1)[0];
+ const match = matches.slice(-1)[0] as AgnosticRouteMatch;
if (!isIndexRequestUrl(url) && match.route.id?.endsWith('/index')) {
- return matches.slice(-2)[0];
+ return matches.slice(-2)[0] as AgnosticRouteMatch;
}
return match;
diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts
index 0a28357e4dea..8450a12eb05d 100644
--- a/packages/remix/src/utils/web-fetch.ts
+++ b/packages/remix/src/utils/web-fetch.ts
@@ -75,8 +75,10 @@ export const normalizeRemixRequest = (request: RemixRequest): Record {
+ const result: Record = {};
+ let iterator: IterableIterator<[string, string]>;
+
+ if (hasIterator(headers)) {
+ iterator = getIterator(headers) as IterableIterator<[string, string]>;
+ } else {
+ return {};
+ }
+
+ for (const [key, value] of iterator) {
+ result[key] = value;
+ }
+ return result;
+}
+
+type IterableType = {
+ [Symbol.iterator]: () => Iterator;
+};
+
+function hasIterator(obj: T): obj is T & IterableType {
+ return obj !== null && typeof (obj as IterableType)[Symbol.iterator] === 'function';
+}
+
+function getIterator(obj: T): Iterator {
+ if (hasIterator(obj)) {
+ return (obj as IterableType)[Symbol.iterator]();
+ }
+ throw new Error('Object does not have an iterator');
+}
diff --git a/packages/remix/test/integration/app_v1/entry.server.tsx b/packages/remix/test/integration/app_v1/entry.server.tsx
index d4ad53d80aec..9ecf5f467588 100644
--- a/packages/remix/test/integration/app_v1/entry.server.tsx
+++ b/packages/remix/test/integration/app_v1/entry.server.tsx
@@ -1,13 +1,6 @@
-// it is important this is first!
-import * as Sentry from '@sentry/remix';
-
-Sentry.init({
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- tracesSampleRate: 1,
- tracePropagationTargets: ['example.org'],
- // Disabling to test series of envelopes deterministically.
- autoSessionTracking: false,
-});
+if (process.env.USE_OTEL !== '1') {
+ require('../instrument.server.cjs');
+}
import type { EntryContext } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
diff --git a/packages/remix/test/integration/app_v2/entry.server.tsx b/packages/remix/test/integration/app_v2/entry.server.tsx
index 04d5ef52f6c2..968ec19a5f59 100644
--- a/packages/remix/test/integration/app_v2/entry.server.tsx
+++ b/packages/remix/test/integration/app_v2/entry.server.tsx
@@ -1,13 +1,8 @@
-// it is important this is first!
-import * as Sentry from '@sentry/remix';
+if (process.env.USE_OTEL !== '1') {
+ require('../instrument.server.cjs');
+}
-Sentry.init({
- dsn: 'https://public@dsn.ingest.sentry.io/1337',
- tracesSampleRate: 1,
- tracePropagationTargets: ['example.org'],
- // Disabling to test series of envelopes deterministically.
- autoSessionTracking: false,
-});
+import * as Sentry from '@sentry/remix';
import type { EntryContext } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
diff --git a/packages/remix/test/integration/app_v2/root.tsx b/packages/remix/test/integration/app_v2/root.tsx
index 15b78b8a6325..399136e04089 100644
--- a/packages/remix/test/integration/app_v2/root.tsx
+++ b/packages/remix/test/integration/app_v2/root.tsx
@@ -8,7 +8,11 @@ export const ErrorBoundary: V2_ErrorBoundaryComponent = () => {
captureRemixErrorBoundaryError(error);
- return error
;
+ return (
+
+
+
+ );
};
export const meta: V2_MetaFunction = ({ data }) => [
diff --git a/packages/remix/test/integration/instrument.server.cjs b/packages/remix/test/integration/instrument.server.cjs
new file mode 100644
index 000000000000..5e1d9e31ab46
--- /dev/null
+++ b/packages/remix/test/integration/instrument.server.cjs
@@ -0,0 +1,10 @@
+const Sentry = require('@sentry/remix');
+
+Sentry.init({
+ dsn: 'https://public@dsn.ingest.sentry.io/1337',
+ tracesSampleRate: 1,
+ tracePropagationTargets: ['example.org'],
+ // Disabling to test series of envelopes deterministically.
+ autoSessionTracking: false,
+ autoInstrumentRemix: process.env.USE_OTEL === '1',
+});
diff --git a/packages/remix/test/integration/jest.config.js b/packages/remix/test/integration/jest.config.js
deleted file mode 100644
index 82c2059da915..000000000000
--- a/packages/remix/test/integration/jest.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-const baseConfig = require('../../jest.config.js');
-
-module.exports = {
- ...baseConfig,
- testMatch: [`${__dirname}/test/server/**/*.test.ts`],
- testPathIgnorePatterns: [`${__dirname}/test/client`],
- forceExit: true,
-};
diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json
index 63560ec64e8b..4f954e1b4eea 100644
--- a/packages/remix/test/integration/package.json
+++ b/packages/remix/test/integration/package.json
@@ -4,7 +4,7 @@
"scripts": {
"build": "remix build",
"dev": "remix dev",
- "start": "remix-serve build"
+ "start":"remix-serve build"
},
"dependencies": {
"@remix-run/express": "1.17.0",
diff --git a/packages/remix/test/integration/test/client/errorboundary.test.ts b/packages/remix/test/integration/test/client/errorboundary.test.ts
index 6d249d563a41..8fdfad307179 100644
--- a/packages/remix/test/integration/test/client/errorboundary.test.ts
+++ b/packages/remix/test/integration/test/client/errorboundary.test.ts
@@ -42,4 +42,9 @@ test('should capture React component errors.', async ({ page }) => {
expect(errorEnvelope.transaction).toBe(
useV2 ? 'routes/error-boundary-capture.$id' : 'routes/error-boundary-capture/$id',
);
+
+ if (useV2) {
+ // The error boundary should be rendered
+ expect(await page.textContent('#error-header')).toBe('ErrorBoundary Error');
+ }
});
diff --git a/packages/remix/test/integration/test/client/meta-tags.test.ts b/packages/remix/test/integration/test/client/meta-tags.test.ts
index 34bf6fec1e01..db919d950a2c 100644
--- a/packages/remix/test/integration/test/client/meta-tags.test.ts
+++ b/packages/remix/test/integration/test/client/meta-tags.test.ts
@@ -42,10 +42,10 @@ test('should send transactions with corresponding `sentry-trace` and `baggage` i
const sentryBaggageContent = await sentryBaggageTag?.getAttribute('content');
expect(sentryTraceContent).toContain(
- `${envelope.contexts?.trace.trace_id}-${envelope.contexts?.trace.parent_span_id}-`,
+ `${envelope.contexts?.trace?.trace_id}-${envelope.contexts?.trace?.parent_span_id}-`,
);
- expect(sentryBaggageContent).toContain(envelope.contexts?.trace.trace_id);
+ expect(sentryBaggageContent).toContain(envelope.contexts?.trace?.trace_id);
});
test('should send transactions with corresponding `sentry-trace` and `baggage` inside a parameterized route', async ({
@@ -59,8 +59,8 @@ test('should send transactions with corresponding `sentry-trace` and `baggage` i
const sentryBaggageContent = await sentryBaggageTag?.getAttribute('content');
expect(sentryTraceContent).toContain(
- `${envelope.contexts?.trace.trace_id}-${envelope.contexts?.trace.parent_span_id}-`,
+ `${envelope.contexts?.trace?.trace_id}-${envelope.contexts?.trace?.parent_span_id}-`,
);
- expect(sentryBaggageContent).toContain(envelope.contexts?.trace.trace_id);
+ expect(sentryBaggageContent).toContain(envelope.contexts?.trace?.trace_id);
});
diff --git a/packages/remix/test/integration/test/server/action.test.ts b/packages/remix/test/integration/test/server/instrumentation-legacy/action.test.ts
similarity index 82%
rename from packages/remix/test/integration/test/server/action.test.ts
rename to packages/remix/test/integration/test/server/instrumentation-legacy/action.test.ts
index 4755523262dc..d9e91088cb8b 100644
--- a/packages/remix/test/integration/test/server/action.test.ts
+++ b/packages/remix/test/integration/test/server/instrumentation-legacy/action.test.ts
@@ -1,16 +1,18 @@
-import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from './utils/helpers';
+import { describe, it } from 'vitest';
+import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from '../utils/helpers';
const useV2 = process.env.REMIX_VERSION === '2';
-jest.spyOn(console, 'error').mockImplementation();
-
-// Repeat tests for each adapter
-describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', adapter => {
+describe('Remix API Actions', () => {
it('correctly instruments a parameterized Remix API action', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/123123`;
- const envelope = await env.getEnvelopeRequest({ url, method: 'post', envelopeType: 'transaction' });
- const transaction = envelope[2];
+ const envelope = await env.getEnvelopeRequest({
+ url,
+ method: 'post',
+ envelopeType: 'transaction',
+ });
+ const transaction = envelope[2]!;
assertSentryTransaction(transaction, {
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
@@ -45,7 +47,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('reports an error thrown from the action', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/-1`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -55,10 +57,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
contexts: {
trace: {
status: 'internal_error',
@@ -69,7 +71,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
},
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/action-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -91,7 +93,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('includes request data in transaction and error events', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/-1`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -101,10 +103,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
request: {
method: 'POST',
@@ -117,7 +119,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
},
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/action-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -140,7 +142,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('handles an error-throwing redirection target', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/-2`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -150,10 +152,9 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction_1, transaction_2] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
-
- assertSentryTransaction(transaction_1[2], {
+ const [transaction_1, transaction_2] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
+ assertSentryTransaction(transaction_1![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -167,7 +168,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryTransaction(transaction_2[2], {
+ assertSentryTransaction(transaction_2![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -181,7 +182,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/action-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -203,7 +204,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('handles a thrown `json()` error response with `statusText`', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/-3`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -213,10 +214,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -230,7 +231,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/action-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -252,7 +253,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('handles a thrown `json()` error response without `statusText`', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/-4`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -262,10 +263,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -279,7 +280,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/action-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -301,7 +302,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('handles a thrown `json()` error response with string body', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/-5`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -311,10 +312,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -328,7 +329,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/action-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -350,7 +351,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('handles a thrown `json()` error response with an empty object', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/action-json-response/-6`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -360,10 +361,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -377,7 +378,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/action-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/action-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -399,7 +400,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('handles thrown string (primitive) from an action', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/server-side-unexpected-errors/-1`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -409,10 +410,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['event', 'transaction'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -426,7 +427,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/server-side-unexpected-errors${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/server-side-unexpected-errors(\/|\.)\$id/),
exception: {
values: [
@@ -448,7 +449,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
});
it('handles thrown object from an action', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/server-side-unexpected-errors/-2`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -458,10 +459,10 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
envelopeType: ['event', 'transaction'],
});
- const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction[2], {
+ assertSentryTransaction(transaction![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -475,7 +476,7 @@ describe.each(['builtin', 'express'])('Remix API Actions with adapter = %s', ada
transaction: `routes/server-side-unexpected-errors${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/server-side-unexpected-errors(\/|\.)\$id/),
exception: {
values: [
diff --git a/packages/remix/test/integration/test/server/loader.test.ts b/packages/remix/test/integration/test/server/instrumentation-legacy/loader.test.ts
similarity index 82%
rename from packages/remix/test/integration/test/server/loader.test.ts
rename to packages/remix/test/integration/test/server/instrumentation-legacy/loader.test.ts
index da046d02f7e5..d97427d4f67a 100644
--- a/packages/remix/test/integration/test/server/loader.test.ts
+++ b/packages/remix/test/integration/test/server/instrumentation-legacy/loader.test.ts
@@ -1,22 +1,20 @@
import { Event } from '@sentry/types';
-import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from './utils/helpers';
+import { describe, expect, it } from 'vitest';
+import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from '../utils/helpers';
const useV2 = process.env.REMIX_VERSION === '2';
-jest.spyOn(console, 'error').mockImplementation();
-
-// Repeat tests for each adapter
-describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', adapter => {
+describe('Remix API Loaders', () => {
it('reports an error thrown from the loader', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/loader-json-response/-2`;
const envelopes = await env.getMultipleEnvelopeRequest({ url, count: 2, envelopeType: ['transaction', 'event'] });
- const event = envelopes[0][2].type === 'transaction' ? envelopes[1][2] : envelopes[0][2];
- const transaction = envelopes[0][2].type === 'transaction' ? envelopes[0][2] : envelopes[1][2];
+ const event = envelopes[0]?.[2]?.type === 'transaction' ? envelopes[1]?.[2] : envelopes[0]?.[2];
+ const transaction = envelopes[0]?.[2]?.type === 'transaction' ? envelopes[0]?.[2] : envelopes[1]?.[2];
- assertSentryTransaction(transaction, {
+ assertSentryTransaction(transaction!, {
contexts: {
trace: {
status: 'internal_error',
@@ -27,7 +25,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
},
});
- assertSentryEvent(event, {
+ assertSentryEvent(event!, {
transaction: expect.stringMatching(/routes\/loader-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -49,17 +47,17 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
});
it('reports a thrown error response the loader', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/loader-throw-response/-1`;
// We also wait for the transaction, even though we don't care about it for this test
// but otherwise this may leak into another test
const envelopes = await env.getMultipleEnvelopeRequest({ url, count: 2, envelopeType: ['event', 'transaction'] });
- const event = envelopes[0][2].type === 'transaction' ? envelopes[1][2] : envelopes[0][2];
- const transaction = envelopes[0][2].type === 'transaction' ? envelopes[0][2] : envelopes[1][2];
+ const event = envelopes[0]?.[2]?.type === 'transaction' ? envelopes[1]?.[2] : envelopes[0]?.[2];
+ const transaction = envelopes[0]?.[2]?.type === 'transaction' ? envelopes[0]?.[2] : envelopes[1]?.[2];
- assertSentryTransaction(transaction, {
+ assertSentryTransaction(transaction!, {
contexts: {
trace: {
status: 'internal_error',
@@ -70,7 +68,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
},
});
- assertSentryEvent(event, {
+ assertSentryEvent(event!, {
transaction: expect.stringMatching(/routes\/loader-throw-response(\/|\.)\$id/),
exception: {
values: [
@@ -92,10 +90,10 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
});
it('correctly instruments a parameterized Remix API loader', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/loader-json-response/123123`;
const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
- const transaction = envelope[2];
+ const transaction = envelope[2]!;
assertSentryTransaction(transaction, {
transaction: `routes/loader-json-response${useV2 ? '.' : '/'}$id`,
@@ -120,7 +118,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
});
it('handles an error-throwing redirection target', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/loader-json-response/-1`;
const envelopes = await env.getMultipleEnvelopeRequest({
@@ -129,10 +127,10 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
envelopeType: ['transaction', 'event'],
});
- const [transaction_1, transaction_2] = envelopes.filter(envelope => envelope[1].type === 'transaction');
- const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+ const [transaction_1, transaction_2] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
- assertSentryTransaction(transaction_1[2], {
+ assertSentryTransaction(transaction_1![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -146,7 +144,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
transaction: `routes/loader-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryTransaction(transaction_2[2], {
+ assertSentryTransaction(transaction_2![2]!, {
contexts: {
trace: {
op: 'http.server',
@@ -160,7 +158,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
transaction: `routes/loader-json-response${useV2 ? '.' : '/'}$id`,
});
- assertSentryEvent(event[2], {
+ assertSentryEvent(event![2]!, {
transaction: expect.stringMatching(/routes\/loader-json-response(\/|\.)\$id/),
exception: {
values: [
@@ -182,7 +180,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
});
it('makes sure scope does not bleed between requests', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const envelopes = await Promise.all([
env.getEnvelopeRequest({ url: `${env.url}/scope-bleed/1`, endServer: false, envelopeType: 'transaction' }),
@@ -194,18 +192,18 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
await new Promise(resolve => env.server.close(resolve));
envelopes.forEach(envelope => {
- const tags = envelope[2].tags as NonNullable;
+ const tags = envelope[2]?.tags as NonNullable;
const customTagArr = Object.keys(tags).filter(t => t.startsWith('tag'));
expect(customTagArr).toHaveLength(1);
- const key = customTagArr[0];
+ const key = customTagArr[0]!;
const val = key[key.length - 1];
expect(tags[key]).toEqual(val);
});
});
it('continues transaction from sentry-trace header and baggage', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/loader-json-response/3`;
// send sentry-trace and baggage headers to loader
@@ -217,11 +215,11 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
});
const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
- expect(envelope[0].trace).toMatchObject({
+ expect(envelope[0]?.trace).toMatchObject({
trace_id: '12312012123120121231201212312012',
});
- assertSentryTransaction(envelope[2], {
+ assertSentryTransaction(envelope![2]!, {
contexts: {
trace: {
trace_id: '12312012123120121231201212312012',
@@ -232,10 +230,10 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
});
it('correctly instruments a deferred loader', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/loader-defer-response/123123`;
const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
- const transaction = envelope[2];
+ const transaction = envelope[2]!;
assertSentryTransaction(transaction, {
transaction: useV2 ? 'routes/loader-defer-response.$id' : 'routes/loader-defer-response/$id',
@@ -275,7 +273,7 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
});
it('does not capture thrown redirect responses', async () => {
- const env = await RemixTestEnv.init(adapter);
+ const env = await RemixTestEnv.init();
const url = `${env.url}/throw-redirect`;
const envelopesCount = await env.countEnvelopes({
diff --git a/packages/remix/test/integration/test/server/instrumentation-legacy/ssr.test.ts b/packages/remix/test/integration/test/server/instrumentation-legacy/ssr.test.ts
new file mode 100644
index 000000000000..e67258b9e14d
--- /dev/null
+++ b/packages/remix/test/integration/test/server/instrumentation-legacy/ssr.test.ts
@@ -0,0 +1,50 @@
+import { describe, expect, it } from 'vitest';
+import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from '../utils/helpers';
+
+const useV2 = process.env.REMIX_VERSION === '2';
+
+describe('Server Side Rendering', () => {
+ it('correctly reports a server side rendering error', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/ssr-error`;
+ const envelopes = await env.getMultipleEnvelopeRequest({ url, count: 2, envelopeType: ['transaction', 'event'] });
+ const [transaction] = envelopes.filter(envelope => envelope[1]?.type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1]?.type === 'event');
+ assertSentryTransaction(transaction![2]!, {
+ contexts: {
+ trace: {
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ tags: useV2
+ ? {
+ // Testing that the wrapped `handleError` correctly adds tags
+ 'remix-test-tag': 'remix-test-value',
+ }
+ : {},
+ });
+
+ assertSentryEvent(event![2]!, {
+ transaction: 'routes/ssr-error',
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Sentry SSR Test Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: useV2 ? 'remix.server.handleError' : 'documentRequest',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+});
diff --git a/packages/remix/test/integration/test/server/instrumentation-otel/action.test.ts b/packages/remix/test/integration/test/server/instrumentation-otel/action.test.ts
new file mode 100644
index 000000000000..a784cd3b8d9c
--- /dev/null
+++ b/packages/remix/test/integration/test/server/instrumentation-otel/action.test.ts
@@ -0,0 +1,495 @@
+import { describe, it } from 'vitest';
+import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from '../utils/helpers';
+
+const useV2 = process.env.REMIX_VERSION === '2';
+
+describe('Remix API Actions', () => {
+ it('correctly instruments a parameterized Remix API action', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/123123`;
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ method: 'post',
+ envelopeType: 'transaction',
+ count: 1,
+ });
+ const transaction = envelopes[0][2];
+
+ assertSentryTransaction(transaction, {
+ transaction: `POST action-json-response/:id`,
+ spans: [
+ {
+ data: {
+ 'code.function': 'action',
+ 'sentry.op': 'action.remix',
+ 'otel.kind': 'INTERNAL',
+ 'match.route.id': `routes/action-json-response${useV2 ? '.' : '/'}$id`,
+ 'match.params.id': '123123',
+ },
+ },
+ {
+ data: {
+ 'code.function': 'loader',
+ 'sentry.op': 'loader.remix',
+ 'otel.kind': 'INTERNAL',
+ 'match.route.id': `routes/action-json-response${useV2 ? '.' : '/'}$id`,
+ 'match.params.id': '123123',
+ },
+ },
+ {
+ data: {
+ 'code.function': 'loader',
+ 'sentry.op': 'loader.remix',
+ 'otel.kind': 'INTERNAL',
+ 'match.route.id': 'root',
+ 'match.params.id': '123123',
+ },
+ },
+ ],
+ request: {
+ method: 'POST',
+ url,
+ cookies: expect.any(Object),
+ headers: {
+ 'user-agent': expect.any(String),
+ host: expect.stringContaining('localhost:'),
+ },
+ },
+ });
+ });
+
+ it('reports an error thrown from the action', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/-1`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ contexts: {
+ trace: {
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Unexpected Server Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: useV2 ? 'remix.server.handleError' : 'action',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('includes request data in transaction and error events', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/-1`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ transaction: `POST action-json-response/:id`,
+ request: {
+ method: 'POST',
+ url,
+ cookies: expect.any(Object),
+ headers: {
+ 'user-agent': expect.any(String),
+ host: expect.stringContaining('localhost:'),
+ },
+ },
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Unexpected Server Error',
+ },
+ ],
+ },
+ request: {
+ method: 'POST',
+ url,
+ cookies: expect.any(Object),
+ headers: {
+ 'user-agent': expect.any(String),
+ host: expect.stringContaining('localhost:'),
+ },
+ },
+ });
+ });
+
+ it('handles an error-throwing redirection target', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/-2`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 3,
+ method: 'post',
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction_1, transaction_2] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction_1[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'ok',
+ data: {
+ 'http.response.status_code': 302,
+ },
+ },
+ },
+ transaction: `POST action-json-response/:id`,
+ });
+
+ assertSentryTransaction(transaction_2[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `GET action-json-response/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Unexpected Server Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: useV2 ? 'remix.server.handleError' : 'loader',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('handles a thrown `json()` error response with `statusText`', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/-3`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `POST action-json-response/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Sentry Test Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: 'action',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('handles a thrown `json()` error response without `statusText`', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/-4`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `POST action-json-response/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Object captured as exception with keys: data',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: 'action',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('handles a thrown `json()` error response with string body', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/-5`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `POST action-json-response/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Sentry Test Error [string body]',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: 'action',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('handles a thrown `json()` error response with an empty object', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/action-json-response/-6`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `POST action-json-response/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Object captured as exception with keys: [object has no keys]',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: 'action',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('handles thrown string (primitive) from an action', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/server-side-unexpected-errors/-1`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['event', 'transaction'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `POST server-side-unexpected-errors/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Thrown String Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: useV2 ? 'remix.server.handleError' : 'action',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('handles thrown object from an action', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/server-side-unexpected-errors/-2`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 2,
+ method: 'post',
+ envelopeType: ['event', 'transaction'],
+ });
+
+ const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `POST server-side-unexpected-errors/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Thrown Object Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: useV2 ? 'remix.server.handleError' : 'action',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+});
diff --git a/packages/remix/test/integration/test/server/instrumentation-otel/loader.test.ts b/packages/remix/test/integration/test/server/instrumentation-otel/loader.test.ts
new file mode 100644
index 000000000000..62e0bf78ac10
--- /dev/null
+++ b/packages/remix/test/integration/test/server/instrumentation-otel/loader.test.ts
@@ -0,0 +1,275 @@
+import { Event } from '@sentry/types';
+import { describe, expect, it } from 'vitest';
+import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from '../utils/helpers';
+
+const useV2 = process.env.REMIX_VERSION === '2';
+
+describe('Remix API Loaders', () => {
+ it('reports an error thrown from the loader', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/loader-json-response/-2`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({ url, count: 2, envelopeType: ['transaction', 'event'] });
+
+ const event = envelopes[0][2].type === 'transaction' ? envelopes[1][2] : envelopes[0][2];
+ const transaction = envelopes[0][2].type === 'transaction' ? envelopes[0][2] : envelopes[1][2];
+
+ assertSentryTransaction(transaction, {
+ contexts: {
+ trace: {
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ });
+
+ assertSentryEvent(event, {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Unexpected Server Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: useV2 ? 'remix.server.handleError' : 'loader',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('reports a thrown error response the loader', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/loader-throw-response/-1`;
+
+ // We also wait for the transaction, even though we don't care about it for this test
+ // but otherwise this may leak into another test
+ const envelopes = await env.getMultipleEnvelopeRequest({ url, count: 2, envelopeType: ['event', 'transaction'] });
+
+ const event = envelopes[0][2].type === 'transaction' ? envelopes[1][2] : envelopes[0][2];
+ const transaction = envelopes[0][2].type === 'transaction' ? envelopes[0][2] : envelopes[1][2];
+
+ assertSentryTransaction(transaction, {
+ contexts: {
+ trace: {
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ });
+
+ assertSentryEvent(event, {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Not found',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: 'loader',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('correctly instruments a parameterized Remix API loader', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/loader-json-response/123123`;
+ const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
+ const transaction = envelope[2];
+
+ assertSentryTransaction(transaction, {
+ transaction: `GET loader-json-response/:id`,
+ transaction_info: {
+ source: 'route',
+ },
+ spans: [
+ {
+ data: {
+ 'code.function': 'loader',
+ 'otel.kind': 'INTERNAL',
+ 'sentry.op': 'loader.remix',
+ },
+ origin: 'manual',
+ },
+ {
+ data: {
+ 'code.function': 'loader',
+ 'otel.kind': 'INTERNAL',
+ 'sentry.op': 'loader.remix',
+ },
+ origin: 'manual',
+ },
+ ],
+ });
+ });
+
+ it('handles an error-throwing redirection target', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/loader-json-response/-1`;
+
+ const envelopes = await env.getMultipleEnvelopeRequest({
+ url,
+ count: 3,
+ envelopeType: ['transaction', 'event'],
+ });
+
+ const [transaction_1, transaction_2] = envelopes.filter(envelope => envelope[1].type === 'transaction');
+ const [event] = envelopes.filter(envelope => envelope[1].type === 'event');
+
+ assertSentryTransaction(transaction_1[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'ok',
+ data: {
+ 'http.response.status_code': 302,
+ },
+ },
+ },
+ transaction: `GET loader-json-response/:id`,
+ });
+
+ assertSentryTransaction(transaction_2[2], {
+ contexts: {
+ trace: {
+ op: 'http.server',
+ status: 'internal_error',
+ data: {
+ 'http.response.status_code': 500,
+ },
+ },
+ },
+ transaction: `GET loader-json-response/:id`,
+ });
+
+ assertSentryEvent(event[2], {
+ exception: {
+ values: [
+ {
+ type: 'Error',
+ value: 'Unexpected Server Error',
+ stacktrace: expect.any(Object),
+ mechanism: {
+ data: {
+ function: useV2 ? 'remix.server.handleError' : 'loader',
+ },
+ handled: false,
+ type: 'instrument',
+ },
+ },
+ ],
+ },
+ });
+ });
+
+ it('makes sure scope does not bleed between requests', async () => {
+ const env = await RemixTestEnv.init();
+
+ const envelopes = await Promise.all([
+ env.getEnvelopeRequest({ url: `${env.url}/scope-bleed/1`, endServer: false, envelopeType: 'transaction' }),
+ env.getEnvelopeRequest({ url: `${env.url}/scope-bleed/2`, endServer: false, envelopeType: 'transaction' }),
+ env.getEnvelopeRequest({ url: `${env.url}/scope-bleed/3`, endServer: false, envelopeType: 'transaction' }),
+ env.getEnvelopeRequest({ url: `${env.url}/scope-bleed/4`, endServer: false, envelopeType: 'transaction' }),
+ ]);
+
+ await new Promise(resolve => env.server.close(resolve));
+
+ envelopes.forEach(envelope => {
+ const tags = envelope[2].tags as NonNullable;
+ const customTagArr = Object.keys(tags).filter(t => t.startsWith('tag'));
+ expect(customTagArr).toHaveLength(1);
+
+ const key = customTagArr[0];
+ const val = key[key.length - 1];
+ expect(tags[key]).toEqual(val);
+ });
+ });
+
+ it('continues transaction from sentry-trace header and baggage', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/loader-json-response/3`;
+
+ // send sentry-trace and baggage headers to loader
+ env.setAxiosConfig({
+ headers: {
+ 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
+ baggage: 'sentry-version=1.0,sentry-environment=production,sentry-trace_id=12312012123120121231201212312012',
+ },
+ });
+ const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
+
+ expect(envelope[0].trace).toMatchObject({
+ trace_id: '12312012123120121231201212312012',
+ });
+
+ assertSentryTransaction(envelope[2], {
+ contexts: {
+ trace: {
+ trace_id: '12312012123120121231201212312012',
+ parent_span_id: '1121201211212012',
+ },
+ },
+ });
+ });
+
+ it('correctly instruments a deferred loader', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/loader-defer-response/123123`;
+ const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
+ const transaction = envelope[2];
+
+ assertSentryTransaction(transaction, {
+ transaction: 'GET loader-defer-response/:id',
+ transaction_info: {
+ source: 'route',
+ },
+ spans: [
+ {
+ data: {
+ 'code.function': 'loader',
+ 'sentry.op': 'loader.remix',
+ 'otel.kind': 'INTERNAL',
+ 'match.route.id': `routes/loader-defer-response${useV2 ? '.' : '/'}$id`,
+ },
+ },
+ {
+ data: {
+ 'code.function': 'loader',
+ 'sentry.op': 'loader.remix',
+ 'otel.kind': 'INTERNAL',
+ 'match.route.id': 'root',
+ },
+ },
+ ],
+ });
+ });
+
+ it('does not capture thrown redirect responses', async () => {
+ const env = await RemixTestEnv.init();
+ const url = `${env.url}/throw-redirect`;
+
+ const envelopesCount = await env.countEnvelopes({
+ url,
+ envelopeType: 'event',
+ timeout: 3000,
+ });
+
+ expect(envelopesCount).toBe(0);
+ });
+});
diff --git a/packages/remix/test/integration/test/server/ssr.test.ts b/packages/remix/test/integration/test/server/instrumentation-otel/ssr.test.ts
similarity index 92%
rename from packages/remix/test/integration/test/server/ssr.test.ts
rename to packages/remix/test/integration/test/server/instrumentation-otel/ssr.test.ts
index 39c49a0d2957..587e57abb1c3 100644
--- a/packages/remix/test/integration/test/server/ssr.test.ts
+++ b/packages/remix/test/integration/test/server/instrumentation-otel/ssr.test.ts
@@ -1,10 +1,11 @@
-import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from './utils/helpers';
+import { describe, expect, it } from 'vitest';
+import { RemixTestEnv, assertSentryEvent, assertSentryTransaction } from '../utils/helpers';
const useV2 = process.env.REMIX_VERSION === '2';
describe('Server Side Rendering', () => {
it('correctly reports a server side rendering error', async () => {
- const env = await RemixTestEnv.init('builtin');
+ const env = await RemixTestEnv.init();
const url = `${env.url}/ssr-error`;
const envelopes = await env.getMultipleEnvelopeRequest({ url, count: 2, envelopeType: ['transaction', 'event'] });
const [transaction] = envelopes.filter(envelope => envelope[1].type === 'transaction');
@@ -28,7 +29,6 @@ describe('Server Side Rendering', () => {
});
assertSentryEvent(event[2], {
- transaction: 'routes/ssr-error',
exception: {
values: [
{
diff --git a/packages/remix/test/integration/test/server/utils/helpers.ts b/packages/remix/test/integration/test/server/utils/helpers.ts
index caf9d5525fd7..eccda209fb48 100644
--- a/packages/remix/test/integration/test/server/utils/helpers.ts
+++ b/packages/remix/test/integration/test/server/utils/helpers.ts
@@ -1,7 +1,6 @@
import * as http from 'http';
import { AddressInfo } from 'net';
import { createRequestHandler } from '@remix-run/express';
-import { wrapExpressCreateRequestHandler } from '@sentry/remix';
import express from 'express';
import { TestEnv } from '../../../../../../../dev-packages/node-integration-tests/utils';
@@ -12,15 +11,12 @@ export class RemixTestEnv extends TestEnv {
super(server, url);
}
- public static async init(adapter: string = 'builtin'): Promise {
- const requestHandlerFactory =
- adapter === 'express' ? wrapExpressCreateRequestHandler(createRequestHandler) : createRequestHandler;
-
+ public static async init(): Promise {
let serverPort;
const server = await new Promise(resolve => {
const app = express();
- app.all('*', requestHandlerFactory({ build: require('../../../build') }));
+ app.all('*', createRequestHandler({ build: require('../../../build') }));
const server = app.listen(0, () => {
serverPort = (server.address() as AddressInfo).port;
diff --git a/packages/remix/test/integration/test/tsconfig.json b/packages/remix/test/integration/test/tsconfig.json
new file mode 100644
index 000000000000..105334e2253a
--- /dev/null
+++ b/packages/remix/test/integration/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../../tsconfig.test.json"
+}
diff --git a/packages/remix/test/integration/tsconfig.test.json b/packages/remix/test/integration/tsconfig.test.json
index d3175b6a1b01..8ce7525d33fd 100644
--- a/packages/remix/test/integration/tsconfig.test.json
+++ b/packages/remix/test/integration/tsconfig.test.json
@@ -4,6 +4,6 @@
"include": ["test/**/*"],
"compilerOptions": {
- "types": ["node", "jest"]
+ "types": ["node", "vitest/globals"]
}
}
diff --git a/packages/remix/test/tsconfig.json b/packages/remix/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/remix/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/remix/test/utils/normalizeRemixRequest.test.ts b/packages/remix/test/utils/normalizeRemixRequest.test.ts
new file mode 100644
index 000000000000..b627a34e4f12
--- /dev/null
+++ b/packages/remix/test/utils/normalizeRemixRequest.test.ts
@@ -0,0 +1,100 @@
+import type { RemixRequest } from '../../src/utils/vendor/types';
+import { normalizeRemixRequest } from '../../src/utils/web-fetch';
+
+class Headers {
+ private _headers: Record = {};
+
+ constructor(headers?: Iterable<[string, string]>) {
+ if (headers) {
+ for (const [key, value] of headers) {
+ this.set(key, value);
+ }
+ }
+ }
+ static fromEntries(entries: Iterable<[string, string]>): Headers {
+ return new Headers(entries);
+ }
+ entries(): IterableIterator<[string, string]> {
+ return Object.entries(this._headers)[Symbol.iterator]();
+ }
+
+ [Symbol.iterator](): IterableIterator<[string, string]> {
+ return this.entries();
+ }
+
+ get(key: string): string | null {
+ return this._headers[key] ?? null;
+ }
+
+ has(key: string): boolean {
+ return this._headers[key] !== undefined;
+ }
+
+ set(key: string, value: string): void {
+ this._headers[key] = value;
+ }
+}
+
+class Request {
+ private _url: string;
+ private _options: { method: string; body?: any; headers: Headers };
+
+ constructor(url: string, options: { method: string; body?: any; headers: Headers }) {
+ this._url = url;
+ this._options = options;
+ }
+
+ get method() {
+ return this._options.method;
+ }
+
+ get url() {
+ return this._url;
+ }
+
+ get headers() {
+ return this._options.headers;
+ }
+
+ get body() {
+ return this._options.body;
+ }
+}
+
+describe('normalizeRemixRequest', () => {
+ it('should normalize remix web-fetch request', () => {
+ const headers = new Headers();
+ headers.set('Accept', 'text/html,application/json');
+ headers.set('Cookie', 'name=value');
+ const request = new Request('https://example.com/api/json?id=123', {
+ method: 'GET',
+ headers: headers as any,
+ });
+
+ const expected = {
+ agent: undefined,
+ hash: '',
+ headers: {
+ Accept: 'text/html,application/json',
+ Connection: 'close',
+ Cookie: 'name=value',
+ 'User-Agent': 'node-fetch',
+ },
+ hostname: 'example.com',
+ href: 'https://example.com/api/json?id=123',
+ insecureHTTPParser: undefined,
+ ip: null,
+ method: 'GET',
+ originalUrl: 'https://example.com/api/json?id=123',
+ path: '/api/json?id=123',
+ pathname: '/api/json',
+ port: '',
+ protocol: 'https:',
+ query: undefined,
+ search: '?id=123',
+ };
+
+ const normalizedRequest = normalizeRemixRequest(request as unknown as RemixRequest);
+ expect(normalizedRequest).toEqual(expected);
+ });
+});
diff --git a/packages/remix/tsconfig.test.json b/packages/remix/tsconfig.test.json
index 7aa20c05d60c..ffcc2b26016c 100644
--- a/packages/remix/tsconfig.test.json
+++ b/packages/remix/tsconfig.test.json
@@ -1,7 +1,7 @@
{
"extends": "./tsconfig.json",
- "include": ["test/**/*"],
+ "include": ["test/**/*", "vitest.config.ts"],
"compilerOptions": {
"types": ["node", "jest"],
diff --git a/packages/remix/vitest.config.ts b/packages/remix/vitest.config.ts
new file mode 100644
index 000000000000..23c2383b9e8b
--- /dev/null
+++ b/packages/remix/vitest.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from 'vitest/config';
+
+const useOtel = process.env.USE_OTEL === '1';
+
+export default defineConfig({
+ test: {
+ globals: true,
+ disableConsoleIntercept: true,
+ silent: false,
+ setupFiles: useOtel ? './test/integration/instrument.server.cjs' : undefined,
+ include: useOtel ? ['**/instrumentation-otel/*.test.ts'] : ['**/instrumentation-legacy/*.test.ts'],
+ },
+});
diff --git a/packages/replay-canvas/test/tsconfig.json b/packages/replay-canvas/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/replay-canvas/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts b/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts
index f7434f595693..fb81b1fd88d0 100644
--- a/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts
+++ b/packages/replay-internal/src/coreHandlers/handleBeforeSendEvent.ts
@@ -1,4 +1,5 @@
import type { ErrorEvent, Event } from '@sentry/types';
+import { getLocationHref } from '@sentry/utils';
import type { ReplayContainer } from '../types';
import { createBreadcrumb } from '../util/createBreadcrumb';
@@ -41,6 +42,9 @@ function handleHydrationError(replay: ReplayContainer, event: ErrorEvent): void
) {
const breadcrumb = createBreadcrumb({
category: 'replay.hydrate-error',
+ data: {
+ url: getLocationHref(),
+ },
});
addBreadcrumbEvent(replay, breadcrumb);
}
diff --git a/packages/replay-internal/src/coreHandlers/util/networkUtils.ts b/packages/replay-internal/src/coreHandlers/util/networkUtils.ts
index b6ae6ff5b13e..06e96b7ab7df 100644
--- a/packages/replay-internal/src/coreHandlers/util/networkUtils.ts
+++ b/packages/replay-internal/src/coreHandlers/util/networkUtils.ts
@@ -189,11 +189,11 @@ export function buildNetworkRequestOrResponse(
/** Filter a set of headers */
export function getAllowedHeaders(headers: Record, allowedHeaders: string[]): Record {
- return Object.keys(headers).reduce((filteredHeaders: Record, key: string) => {
+ return Object.entries(headers).reduce((filteredHeaders: Record, [key, value]) => {
const normalizedKey = key.toLowerCase();
// Avoid putting empty strings into the headers
if (allowedHeaders.includes(normalizedKey) && headers[key]) {
- filteredHeaders[normalizedKey] = headers[key];
+ filteredHeaders[normalizedKey] = value;
}
return filteredHeaders;
}, {});
diff --git a/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts b/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts
index fa504dcdeec2..b86e2d2991a9 100644
--- a/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts
+++ b/packages/replay-internal/src/coreHandlers/util/xhrUtils.ts
@@ -136,8 +136,10 @@ function getResponseHeaders(xhr: XMLHttpRequest): Record {
}
return headers.split('\r\n').reduce((acc: Record, line: string) => {
- const [key, value] = line.split(': ');
- acc[key.toLowerCase()] = value;
+ const [key, value] = line.split(': ') as [string, string | undefined];
+ if (value) {
+ acc[key.toLowerCase()] = value;
+ }
return acc;
}, {});
}
diff --git a/packages/replay-internal/src/types/replayFrame.ts b/packages/replay-internal/src/types/replayFrame.ts
index 6a7a1a8e255a..0fa43ff41eb2 100644
--- a/packages/replay-internal/src/types/replayFrame.ts
+++ b/packages/replay-internal/src/types/replayFrame.ts
@@ -68,6 +68,14 @@ interface ReplayMutationFrame extends ReplayBaseBreadcrumbFrame {
data: ReplayMutationFrameData;
}
+interface ReplayHydrationErrorFrameData {
+ url: string;
+}
+interface ReplayHydrationErrorFrame extends ReplayBaseBreadcrumbFrame {
+ category: 'replay.hydrate-error';
+ data: ReplayHydrationErrorFrameData;
+}
+
interface ReplayKeyboardEventFrameData extends ReplayBaseDomFrameData {
metaKey: boolean;
shiftKey: boolean;
@@ -146,6 +154,7 @@ export type ReplayBreadcrumbFrame =
| ReplaySlowClickFrame
| ReplayMultiClickFrame
| ReplayMutationFrame
+ | ReplayHydrationErrorFrame
| ReplayFeedbackFrame
| ReplayBaseBreadcrumbFrame;
diff --git a/packages/replay-internal/src/util/createPerformanceEntries.ts b/packages/replay-internal/src/util/createPerformanceEntries.ts
index b7cca6b05ddf..28ccf60280e8 100644
--- a/packages/replay-internal/src/util/createPerformanceEntries.ts
+++ b/packages/replay-internal/src/util/createPerformanceEntries.ts
@@ -72,11 +72,12 @@ export function createPerformanceEntries(
}
function createPerformanceEntry(entry: AllPerformanceEntry): ReplayPerformanceEntry | null {
- if (!ENTRY_TYPES[entry.entryType]) {
+ const entryType = ENTRY_TYPES[entry.entryType];
+ if (!entryType) {
return null;
}
- return ENTRY_TYPES[entry.entryType](entry);
+ return entryType(entry);
}
function getAbsoluteTime(time: number): number {
@@ -192,7 +193,11 @@ export function getLargestContentfulPaint(metric: Metric): ReplayPerformanceEntr
export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry {
// get first node that shifts
const firstEntry = metric.entries[0] as (PerformanceEntry & { sources?: LayoutShiftAttribution[] }) | undefined;
- const node = firstEntry ? (firstEntry.sources ? firstEntry.sources[0].node : undefined) : undefined;
+ const node = firstEntry
+ ? firstEntry.sources && firstEntry.sources[0]
+ ? firstEntry.sources[0].node
+ : undefined
+ : undefined;
return getWebVital(metric, 'cumulative-layout-shift', node);
}
diff --git a/packages/replay-internal/test/integration/coreHandlers/handleBeforeSendEvent.test.ts b/packages/replay-internal/test/integration/coreHandlers/handleBeforeSendEvent.test.ts
index 8cd9fda46247..39c53cf91e6f 100644
--- a/packages/replay-internal/test/integration/coreHandlers/handleBeforeSendEvent.test.ts
+++ b/packages/replay-internal/test/integration/coreHandlers/handleBeforeSendEvent.test.ts
@@ -29,7 +29,7 @@ describe('Integration | coreHandlers | handleBeforeSendEvent', () => {
const addBreadcrumbSpy = vi.spyOn(replay, 'throttledAddEvent');
const error = Error();
- error.exception.values[0].value =
+ error.exception.values[0]!.value =
'Text content does not match server-rendered HTML. Warning: Text content did not match.';
handler(error);
@@ -38,6 +38,7 @@ describe('Integration | coreHandlers | handleBeforeSendEvent', () => {
data: {
payload: {
category: 'replay.hydrate-error',
+ data: { url: 'http://localhost:3000/' },
timestamp: expect.any(Number),
type: 'default',
},
@@ -63,7 +64,7 @@ describe('Integration | coreHandlers | handleBeforeSendEvent', () => {
const addBreadcrumbSpy = vi.spyOn(replay, 'throttledAddEvent');
const error = Error();
- error.exception.values[0].value = 'https://reactjs.org/docs/error-decoder.html?invariant=423';
+ error.exception.values[0]!.value = 'https://reactjs.org/docs/error-decoder.html?invariant=423';
handler(error);
expect(addBreadcrumbSpy).toHaveBeenCalledTimes(1);
@@ -71,6 +72,7 @@ describe('Integration | coreHandlers | handleBeforeSendEvent', () => {
data: {
payload: {
category: 'replay.hydrate-error',
+ data: { url: 'http://localhost:3000/' },
timestamp: expect.any(Number),
type: 'default',
},
diff --git a/packages/replay-internal/test/integration/sendReplayEvent.test.ts b/packages/replay-internal/test/integration/sendReplayEvent.test.ts
index 8e99f72ff517..9b318700a6fa 100644
--- a/packages/replay-internal/test/integration/sendReplayEvent.test.ts
+++ b/packages/replay-internal/test/integration/sendReplayEvent.test.ts
@@ -395,7 +395,7 @@ describe('Integration | sendReplayEvent', () => {
expect(spyHandleException).toHaveBeenLastCalledWith(new Error('Unable to send Replay - max retries exceeded'));
const spyHandleExceptionCall = spyHandleException.mock.calls;
- expect(spyHandleExceptionCall[spyHandleExceptionCall.length - 1][0].cause.message).toEqual(
+ expect(spyHandleExceptionCall[spyHandleExceptionCall.length - 1][0]?.cause.message).toEqual(
'Something bad happened',
);
diff --git a/packages/replay-internal/test/integration/session.test.ts b/packages/replay-internal/test/integration/session.test.ts
index fb1327296ad2..397dd66d119e 100644
--- a/packages/replay-internal/test/integration/session.test.ts
+++ b/packages/replay-internal/test/integration/session.test.ts
@@ -120,7 +120,7 @@ describe('Integration | session', () => {
const initialSession = { ...replay.session } as Session;
expect(mockRecord).toHaveBeenCalledTimes(1);
- expect(initialSession?.id).toBeDefined();
+ expect(initialSession.id).toBeDefined();
expect(replay.getContext()).toEqual(
expect.objectContaining({
initialUrl: 'http://localhost:3000/',
@@ -230,7 +230,7 @@ describe('Integration | session', () => {
it('pauses and resumes a session if user has been idle for more than SESSION_IDLE_PASUE_DURATION and comes back to click their mouse', async () => {
const initialSession = { ...replay.session } as Session;
- expect(initialSession?.id).toBeDefined();
+ expect(initialSession.id).toBeDefined();
expect(replay.getContext()).toEqual(
expect.objectContaining({
initialUrl: 'http://localhost:3000/',
@@ -327,7 +327,7 @@ describe('Integration | session', () => {
const initialSession = { ...replay.session } as Session;
- expect(initialSession?.id).toBeDefined();
+ expect(initialSession.id).toBeDefined();
expect(replay.getContext()).toMatchObject({
initialUrl: 'http://localhost:3000/',
initialTimestamp: BASE_TIMESTAMP,
diff --git a/packages/replay-internal/test/tsconfig.json b/packages/replay-internal/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/replay-internal/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts b/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
index ddf638c8498f..1271a557a744 100644
--- a/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
+++ b/packages/replay-internal/test/unit/coreHandlers/handleNetworkBreadcrumbs.test.ts
@@ -37,10 +37,10 @@ function getMockResponse(contentLength?: string, body?: string, headers?: Record
const response = {
headers: {
has: (prop: string) => {
- return !!internalHeaders[prop?.toLowerCase() ?? ''];
+ return !!internalHeaders[prop.toLowerCase() ?? ''];
},
get: (prop: string) => {
- return internalHeaders[prop?.toLowerCase() ?? ''];
+ return internalHeaders[prop.toLowerCase() ?? ''];
},
},
clone: () => response,
diff --git a/packages/replay-worker/src/Compressor.ts b/packages/replay-worker/src/Compressor.ts
index 80a5fa1822ac..712481aa62d7 100644
--- a/packages/replay-worker/src/Compressor.ts
+++ b/packages/replay-worker/src/Compressor.ts
@@ -90,15 +90,16 @@ function mergeUInt8Arrays(chunks: Uint8Array[]): Uint8Array {
// calculate data length
let len = 0;
- for (let i = 0, l = chunks.length; i < l; i++) {
- len += chunks[i].length;
+ for (const chunk of chunks) {
+ len += chunk.length;
}
// join chunks
const result = new Uint8Array(len);
for (let i = 0, pos = 0, l = chunks.length; i < l; i++) {
- const chunk = chunks[i];
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const chunk = chunks[i]!;
result.set(chunk, pos);
pos += chunk.length;
}
diff --git a/packages/replay-worker/test/tsconfig.json b/packages/replay-worker/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/replay-worker/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/solid/test/tsconfig.json b/packages/solid/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/solid/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/svelte/src/preprocessors.ts b/packages/svelte/src/preprocessors.ts
index a258d4becff4..c966c6e00eef 100644
--- a/packages/svelte/src/preprocessors.ts
+++ b/packages/svelte/src/preprocessors.ts
@@ -116,7 +116,8 @@ function shouldInjectFunction(
function getBaseName(filename: string): string {
const segments = filename.split('/');
- return segments[segments.length - 1].replace('.svelte', '');
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return segments[segments.length - 1]!.replace('.svelte', '');
}
function hasScriptTag(content: string): boolean {
diff --git a/packages/svelte/test/config.test.ts b/packages/svelte/test/config.test.ts
index c571ef6fb74e..7d1255e120d0 100644
--- a/packages/svelte/test/config.test.ts
+++ b/packages/svelte/test/config.test.ts
@@ -42,7 +42,7 @@ describe('withSentryConfig', () => {
expect(Array.isArray(wrappedConfig.preprocess)).toBe(true);
expect(wrappedConfig).toEqual({ ...originalConfig, preprocess: expect.any(Array) });
expect(wrappedConfig.preprocess).toHaveLength(originalNumberOfPreprocs + 1);
- expect((wrappedConfig.preprocess as SentryPreprocessorGroup[])[0].sentryId).toEqual(
+ expect((wrappedConfig.preprocess as SentryPreprocessorGroup[])[0]?.sentryId).toEqual(
FIRST_PASS_COMPONENT_TRACKING_PREPROC_ID,
);
});
diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts
index 99ac99698682..758207797284 100644
--- a/packages/svelte/test/performance.test.ts
+++ b/packages/svelte/test/performance.test.ts
@@ -46,7 +46,7 @@ describe('Sentry.trackComponent()', () => {
const rootSpanId = transaction.contexts?.trace?.span_id;
expect(rootSpanId).toBeDefined();
- const initSpanId = transaction.spans![0].span_id;
+ const initSpanId = transaction.spans![0]?.span_id;
expect(transaction.spans![0]).toEqual({
data: {
@@ -101,7 +101,7 @@ describe('Sentry.trackComponent()', () => {
const rootSpanId = transaction.contexts?.trace?.span_id;
expect(rootSpanId).toBeDefined();
- const initSpanId = transaction.spans![0].span_id;
+ const initSpanId = transaction.spans![0]?.span_id;
expect(transaction.spans![0]).toEqual({
data: {
@@ -162,7 +162,7 @@ describe('Sentry.trackComponent()', () => {
const transaction = transactions[0];
expect(transaction.spans).toHaveLength(1);
- expect(transaction.spans![0].op).toEqual('ui.svelte.init');
+ expect(transaction.spans![0]?.op).toEqual('ui.svelte.init');
});
it('only creates update spans if trackInit is deactivated', async () => {
@@ -178,7 +178,7 @@ describe('Sentry.trackComponent()', () => {
const transaction = transactions[0];
expect(transaction.spans).toHaveLength(1);
- expect(transaction.spans![0].op).toEqual('ui.svelte.update');
+ expect(transaction.spans![0]?.op).toEqual('ui.svelte.update');
});
it('creates no spans if trackInit and trackUpdates are deactivated', async () => {
@@ -210,8 +210,8 @@ describe('Sentry.trackComponent()', () => {
const transaction = transactions[0];
expect(transaction.spans).toHaveLength(2);
- expect(transaction.spans![0].description).toEqual('');
- expect(transaction.spans![1].description).toEqual('');
+ expect(transaction.spans![0]?.description).toEqual('');
+ expect(transaction.spans![1]?.description).toEqual('');
});
it("doesn't do anything, if there's no ongoing transaction", async () => {
@@ -243,7 +243,7 @@ describe('Sentry.trackComponent()', () => {
// One update span is triggered by the initial rendering, but the second one is not captured
expect(transaction.spans).toHaveLength(2);
- expect(transaction.spans![0].op).toEqual('ui.svelte.init');
- expect(transaction.spans![1].op).toEqual('ui.svelte.update');
+ expect(transaction.spans![0]?.op).toEqual('ui.svelte.init');
+ expect(transaction.spans![1]?.op).toEqual('ui.svelte.update');
});
});
diff --git a/packages/svelte/test/tsconfig.json b/packages/svelte/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/svelte/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/sveltekit/test/tsconfig.json b/packages/sveltekit/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/sveltekit/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts
index b4609ec0568c..bb0ec4646211 100644
--- a/packages/types/src/client.ts
+++ b/packages/types/src/client.ts
@@ -244,7 +244,7 @@ export interface Client {
/**
* Register a callback when a DSC (Dynamic Sampling Context) is created.
*/
- on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): void;
+ on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext, rootSpan?: Span) => void): void;
/**
* Register a callback when a Feedback event has been prepared.
@@ -338,7 +338,7 @@ export interface Client {
/**
* Fire a hook for when a DSC (Dynamic Sampling Context) is created. Expects the DSC as second argument.
*/
- emit(hook: 'createDsc', dsc: DynamicSamplingContext): void;
+ emit(hook: 'createDsc', dsc: DynamicSamplingContext, rootSpan?: Span): void;
/**
* Fire a hook event for after preparing a feedback event. Events to be given
diff --git a/packages/types/src/feedback/config.ts b/packages/types/src/feedback/config.ts
index e6f92e65d52e..2350545941be 100644
--- a/packages/types/src/feedback/config.ts
+++ b/packages/types/src/feedback/config.ts
@@ -1,3 +1,4 @@
+import type { Primitive } from '../misc';
import type { FeedbackFormData } from './form';
import type { FeedbackTheme } from './theme';
@@ -55,6 +56,11 @@ export interface FeedbackGeneralConfiguration {
email: string;
name: string;
};
+
+ /**
+ * Set an object that will be merged sent as tags data with the event.
+ */
+ tags?: { [key: string]: Primitive };
}
/**
diff --git a/packages/types/src/feedback/sendFeedback.ts b/packages/types/src/feedback/sendFeedback.ts
index a284e82f107b..8f865b57038d 100644
--- a/packages/types/src/feedback/sendFeedback.ts
+++ b/packages/types/src/feedback/sendFeedback.ts
@@ -1,4 +1,5 @@
import type { Event, EventHint } from '../event';
+import type { Primitive } from '../misc';
import type { User } from '../user';
/**
@@ -38,13 +39,14 @@ export interface SendFeedbackParams {
url?: string;
source?: string;
associatedEventId?: string;
-}
-interface SendFeedbackOptions extends EventHint {
/**
- * Should include replay with the feedback?
+ * Set an object that will be merged sent as tags data with the event.
*/
- includeReplay?: boolean;
+ tags?: { [key: string]: Primitive };
}
-export type SendFeedback = (params: SendFeedbackParams, options?: SendFeedbackOptions) => Promise;
+export type SendFeedback = (
+ params: SendFeedbackParams,
+ hint?: EventHint & { includeReplay?: boolean },
+) => Promise;
diff --git a/packages/types/src/profiling.ts b/packages/types/src/profiling.ts
index 5161b6b64b2e..48dd797492bf 100644
--- a/packages/types/src/profiling.ts
+++ b/packages/types/src/profiling.ts
@@ -50,7 +50,6 @@ export interface ContinuousThreadCpuProfile {
}
interface BaseProfile {
- timestamp: string;
version: string;
release: string;
environment: string;
diff --git a/packages/typescript/tsconfig.json b/packages/typescript/tsconfig.json
index d2457663e802..bee7b140cf96 100644
--- a/packages/typescript/tsconfig.json
+++ b/packages/typescript/tsconfig.json
@@ -19,6 +19,7 @@
"sourceMap": true,
"strict": true,
"strictBindCallApply": false,
- "target": "es2018"
+ "target": "es2018",
+ "noUncheckedIndexedAccess": true
}
}
diff --git a/packages/utils/src/baggage.ts b/packages/utils/src/baggage.ts
index b0e506b8938a..8cc2dfd68ef2 100644
--- a/packages/utils/src/baggage.ts
+++ b/packages/utils/src/baggage.ts
@@ -97,9 +97,9 @@ export function parseBaggageHeader(
// Combine all baggage headers into one object containing the baggage values so we can later read the Sentry-DSC-values from it
return baggageHeader.reduce>((acc, curr) => {
const currBaggageObject = baggageHeaderToObject(curr);
- for (const key of Object.keys(currBaggageObject)) {
- acc[key] = currBaggageObject[key];
- }
+ Object.entries(currBaggageObject).forEach(([key, value]) => {
+ acc[key] = value;
+ });
return acc;
}, {});
}
@@ -118,7 +118,9 @@ function baggageHeaderToObject(baggageHeader: string): Record {
.split(',')
.map(baggageEntry => baggageEntry.split('=').map(keyOrValue => decodeURIComponent(keyOrValue.trim())))
.reduce>((acc, [key, value]) => {
- acc[key] = value;
+ if (key && value) {
+ acc[key] = value;
+ }
return acc;
}, {});
}
diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts
index 371e7e96c8c2..ce00f2556d05 100644
--- a/packages/utils/src/browser.ts
+++ b/packages/utils/src/browser.ts
@@ -75,11 +75,6 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string {
};
const out = [];
- let className;
- let classes;
- let key;
- let attr;
- let i;
if (!elem || !elem.tagName) {
return '';
@@ -115,22 +110,22 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string {
out.push(`#${elem.id}`);
}
- className = elem.className;
+ const className = elem.className;
if (className && isString(className)) {
- classes = className.split(/\s+/);
- for (i = 0; i < classes.length; i++) {
- out.push(`.${classes[i]}`);
+ const classes = className.split(/\s+/);
+ for (const c of classes) {
+ out.push(`.${c}`);
}
}
}
const allowedAttrs = ['aria-label', 'type', 'name', 'title', 'alt'];
- for (i = 0; i < allowedAttrs.length; i++) {
- key = allowedAttrs[i];
- attr = elem.getAttribute(key);
+ for (const k of allowedAttrs) {
+ const attr = elem.getAttribute(k);
if (attr) {
- out.push(`[${key}="${attr}"]`);
+ out.push(`[${k}="${attr}"]`);
}
}
+
return out.join('');
}
diff --git a/packages/utils/src/dsn.ts b/packages/utils/src/dsn.ts
index 7bf735c10780..5ca4aa96c180 100644
--- a/packages/utils/src/dsn.ts
+++ b/packages/utils/src/dsn.ts
@@ -45,7 +45,7 @@ export function dsnFromString(str: string): DsnComponents | undefined {
return undefined;
}
- const [protocol, publicKey, pass = '', host, port = '', lastPath] = match.slice(1);
+ const [protocol, publicKey, pass = '', host = '', port = '', lastPath = ''] = match.slice(1);
let path = '';
let projectId = lastPath;
diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts
index c8afc0818909..ee48a2d60c2d 100644
--- a/packages/utils/src/misc.ts
+++ b/packages/utils/src/misc.ts
@@ -38,7 +38,8 @@ export function uuid4(): string {
// @see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#typedarray
const typedArray = new Uint8Array(1);
crypto.getRandomValues(typedArray);
- return typedArray[0];
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return typedArray[0]!;
};
}
} catch (_) {
@@ -135,15 +136,19 @@ interface SemVer {
buildmetadata?: string;
}
+function _parseInt(input: string | undefined): number {
+ return parseInt(input || '', 10);
+}
+
/**
* Parses input into a SemVer interface
* @param input string representation of a semver version
*/
export function parseSemver(input: string): SemVer {
const match = input.match(SEMVER_REGEXP) || [];
- const major = parseInt(match[1], 10);
- const minor = parseInt(match[2], 10);
- const patch = parseInt(match[3], 10);
+ const major = _parseInt(match[1]);
+ const minor = _parseInt(match[2]);
+ const patch = _parseInt(match[3]);
return {
buildmetadata: match[5],
major: isNaN(major) ? undefined : major,
@@ -173,7 +178,11 @@ export function addContextToFrame(lines: string[], frame: StackFrame, linesOfCon
.slice(Math.max(0, sourceLine - linesOfContext), sourceLine)
.map((line: string) => snipLine(line, 0));
- frame.context_line = snipLine(lines[Math.min(maxLines - 1, sourceLine)], frame.colno || 0);
+ // We guard here to ensure this is not larger than the existing number of lines
+ const lineIndex = Math.min(maxLines - 1, sourceLine);
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ frame.context_line = snipLine(lines[lineIndex]!, frame.colno || 0);
frame.post_context = lines
.slice(Math.min(sourceLine + 1, maxLines), sourceLine + 1 + linesOfContext)
diff --git a/packages/utils/src/node-stack-trace.ts b/packages/utils/src/node-stack-trace.ts
index 6bb1877c870d..7748046528a4 100644
--- a/packages/utils/src/node-stack-trace.ts
+++ b/packages/utils/src/node-stack-trace.ts
@@ -116,9 +116,9 @@ export function node(getModule?: GetModuleFn): StackLineParserFn {
filename,
module: getModule ? getModule(filename) : undefined,
function: functionName,
- lineno: parseInt(lineMatch[3], 10) || undefined,
- colno: parseInt(lineMatch[4], 10) || undefined,
- in_app: filenameIsInApp(filename, isNative),
+ lineno: _parseIntOrUndefined(lineMatch[3]),
+ colno: _parseIntOrUndefined(lineMatch[4]),
+ in_app: filenameIsInApp(filename || '', isNative),
};
}
@@ -141,3 +141,7 @@ export function node(getModule?: GetModuleFn): StackLineParserFn {
export function nodeStackLineParser(getModule?: GetModuleFn): StackLineParser {
return [90, node(getModule)];
}
+
+function _parseIntOrUndefined(input: string | undefined): number | undefined {
+ return parseInt(input || '', 10) || undefined;
+}
diff --git a/packages/utils/src/object.ts b/packages/utils/src/object.ts
index 9ae3e00e05ce..95346cf1f812 100644
--- a/packages/utils/src/object.ts
+++ b/packages/utils/src/object.ts
@@ -182,12 +182,14 @@ export function extractExceptionKeysForMessage(exception: Record= maxLength) {
- return truncate(keys[0], maxLength);
+ if (firstKey.length >= maxLength) {
+ return truncate(firstKey, maxLength);
}
for (let includedKeys = keys.length; includedKeys > 0; includedKeys--) {
diff --git a/packages/utils/src/path.ts b/packages/utils/src/path.ts
index 7a65aa57b7c8..ed14e85a3a92 100644
--- a/packages/utils/src/path.ts
+++ b/packages/utils/src/path.ts
@@ -183,7 +183,7 @@ export function join(...args: string[]): string {
/** JSDoc */
export function dirname(path: string): string {
const result = splitPath(path);
- const root = result[0];
+ const root = result[0] || '';
let dir = result[1];
if (!root && !dir) {
@@ -201,7 +201,7 @@ export function dirname(path: string): string {
/** JSDoc */
export function basename(path: string, ext?: string): string {
- let f = splitPath(path)[2];
+ let f = splitPath(path)[2] || '';
if (ext && f.slice(ext.length * -1) === ext) {
f = f.slice(0, f.length - ext.length);
}
diff --git a/packages/utils/src/promisebuffer.ts b/packages/utils/src/promisebuffer.ts
index 0ad4991cfb48..32b90e4d8519 100644
--- a/packages/utils/src/promisebuffer.ts
+++ b/packages/utils/src/promisebuffer.ts
@@ -26,8 +26,8 @@ export function makePromiseBuffer(limit?: number): PromiseBuffer {
* @param task Can be any PromiseLike
* @returns Removed promise.
*/
- function remove(task: PromiseLike): PromiseLike {
- return buffer.splice(buffer.indexOf(task), 1)[0];
+ function remove(task: PromiseLike): PromiseLike {
+ return buffer.splice(buffer.indexOf(task), 1)[0] || Promise.resolve(undefined);
}
/**
diff --git a/packages/utils/src/ratelimit.ts b/packages/utils/src/ratelimit.ts
index 77be8cf7fa9e..6131cff2bb2c 100644
--- a/packages/utils/src/ratelimit.ts
+++ b/packages/utils/src/ratelimit.ts
@@ -78,7 +78,7 @@ export function updateRateLimits(
* Only present if rate limit applies to the metric_bucket data category.
*/
for (const limit of rateLimitHeader.trim().split(',')) {
- const [retryAfter, categories, , , namespaces] = limit.split(':', 5);
+ const [retryAfter, categories, , , namespaces] = limit.split(':', 5) as [string, ...string[]];
const headerDelay = parseInt(retryAfter, 10);
const delay = (!isNaN(headerDelay) ? headerDelay : 60) * 1000; // 60sec default
if (!categories) {
diff --git a/packages/utils/src/stacktrace.ts b/packages/utils/src/stacktrace.ts
index dfb2a6e6269f..7fddda6db158 100644
--- a/packages/utils/src/stacktrace.ts
+++ b/packages/utils/src/stacktrace.ts
@@ -21,7 +21,7 @@ export function createStackParser(...parsers: StackLineParser[]): StackParser {
const lines = stack.split('\n');
for (let i = skipFirstLines; i < lines.length; i++) {
- const line = lines[i];
+ const line = lines[i] as string;
// Ignore lines over 1kb as they are unlikely to be stack frames.
// Many of the regular expressions use backtracking which results in run time that increases exponentially with
// input size. Huge strings can result in hangs/Denial of Service:
@@ -85,7 +85,7 @@ export function stripSentryFramesAndReverse(stack: ReadonlyArray): S
const localStack = Array.from(stack);
// If stack starts with one of our API calls, remove it (starts, meaning it's the top of the stack - aka last call)
- if (/sentryWrapped/.test(localStack[localStack.length - 1].function || '')) {
+ if (/sentryWrapped/.test(getLastStackFrame(localStack).function || '')) {
localStack.pop();
}
@@ -93,7 +93,7 @@ export function stripSentryFramesAndReverse(stack: ReadonlyArray): S
localStack.reverse();
// If stack ends with one of our internal API calls, remove it (ends, meaning it's the bottom of the stack - aka top-most call)
- if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) {
+ if (STRIP_FRAME_REGEXP.test(getLastStackFrame(localStack).function || '')) {
localStack.pop();
// When using synthetic events, we will have a 2 levels deep stack, as `new Error('Sentry syntheticException')`
@@ -104,18 +104,22 @@ export function stripSentryFramesAndReverse(stack: ReadonlyArray): S
//
// instead of just the top `Sentry` call itself.
// This forces us to possibly strip an additional frame in the exact same was as above.
- if (STRIP_FRAME_REGEXP.test(localStack[localStack.length - 1].function || '')) {
+ if (STRIP_FRAME_REGEXP.test(getLastStackFrame(localStack).function || '')) {
localStack.pop();
}
}
return localStack.slice(0, STACKTRACE_FRAME_LIMIT).map(frame => ({
...frame,
- filename: frame.filename || localStack[localStack.length - 1].filename,
+ filename: frame.filename || getLastStackFrame(localStack).filename,
function: frame.function || UNKNOWN_FUNCTION,
}));
}
+function getLastStackFrame(arr: StackFrame[]): StackFrame {
+ return arr[arr.length - 1] || {};
+}
+
const defaultFunctionName = '';
/**
diff --git a/packages/utils/src/url.ts b/packages/utils/src/url.ts
index b7b11138d3c7..e324f41f82a3 100644
--- a/packages/utils/src/url.ts
+++ b/packages/utils/src/url.ts
@@ -45,8 +45,7 @@ export function parseUrl(url: string): PartialURL {
* @returns URL or path without query string or fragment
*/
export function stripUrlQueryAndFragment(urlPath: string): string {
- // eslint-disable-next-line no-useless-escape
- return urlPath.split(/[\?#]/, 1)[0];
+ return (urlPath.split(/[?#]/, 1) as [string, ...string[]])[0];
}
/**
diff --git a/packages/utils/src/worldwide.ts b/packages/utils/src/worldwide.ts
index 0263fc5ec719..7e428444e21d 100644
--- a/packages/utils/src/worldwide.ts
+++ b/packages/utils/src/worldwide.ts
@@ -24,7 +24,7 @@ interface SentryCarrier {
globalScope?: Scope;
defaultIsolationScope?: Scope;
defaultCurrentScope?: Scope;
- globalMetricsAggregators: WeakMap | undefined;
+ globalMetricsAggregators?: WeakMap | undefined;
/** Overwrites TextEncoder used in `@sentry/utils`, need for `react-native@0.73` and older */
encodePolyfill?: (input: string) => Uint8Array;
diff --git a/packages/utils/test/aggregate-errors.test.ts b/packages/utils/test/aggregate-errors.test.ts
index 8d5fb3be6ded..7cfd8e0b82da 100644
--- a/packages/utils/test/aggregate-errors.test.ts
+++ b/packages/utils/test/aggregate-errors.test.ts
@@ -132,7 +132,7 @@ describe('applyAggregateErrorsToEvent()', () => {
expect(event.exception?.values).toHaveLength(5 + 1);
// Last exception in list should be the root exception
- expect(event.exception?.values?.[event.exception?.values.length - 1]).toStrictEqual({
+ expect(event.exception?.values?.[event.exception.values.length - 1]).toStrictEqual({
type: 'Error',
value: 'Root Error',
mechanism: {
@@ -153,7 +153,7 @@ describe('applyAggregateErrorsToEvent()', () => {
const eventHint: EventHint = { originalException: fakeAggregateError };
applyAggregateErrorsToEvent(exceptionFromError, stackParser, undefined, 'cause', 100, event, eventHint);
- expect(event.exception?.values?.[event.exception.values.length - 1].mechanism?.type).toBe('instrument');
+ expect(event.exception?.values?.[event.exception.values.length - 1]?.mechanism?.type).toBe('instrument');
});
test('should recursively walk mixed errors (Aggregate errors and based on `key`)', () => {
diff --git a/packages/utils/test/clientreport.test.ts b/packages/utils/test/clientreport.test.ts
index d54ec311d839..04a2a4fe7334 100644
--- a/packages/utils/test/clientreport.test.ts
+++ b/packages/utils/test/clientreport.test.ts
@@ -33,7 +33,7 @@ describe('createClientReportEnvelope', () => {
const items = env[1];
expect(items).toHaveLength(1);
- const clientReportItem = items[0];
+ const clientReportItem = items[0]!;
expect(clientReportItem[0]).toEqual({ type: 'client_report' });
expect(clientReportItem[1]).toEqual({ timestamp: expect.any(Number), discarded_events: discardedEvents });
diff --git a/packages/utils/test/envelope.test.ts b/packages/utils/test/envelope.test.ts
index 6f3a9ea3a6d6..fa2df9cc159a 100644
--- a/packages/utils/test/envelope.test.ts
+++ b/packages/utils/test/envelope.test.ts
@@ -71,7 +71,7 @@ describe('envelope', () => {
measurements: { inp: { value: expect.any(Number), unit: expect.any(String) } },
};
- expect(spanEnvelopeItem[0].type).toBe('span');
+ expect(spanEnvelopeItem[0]?.type).toBe('span');
expect(spanEnvelopeItem[1]).toMatchObject(expectedObj);
});
});
@@ -209,7 +209,7 @@ describe('envelope', () => {
let iteration = 0;
forEachEnvelopeItem(env, (item, type) => {
expect(item).toBe(items[iteration]);
- expect(type).toBe(items[iteration][0].type);
+ expect(type).toBe(items[iteration]?.[0]?.type);
iteration = iteration + 1;
});
});
diff --git a/packages/utils/test/eventbuilder.test.ts b/packages/utils/test/eventbuilder.test.ts
index c09697366b6f..7c67571e753e 100644
--- a/packages/utils/test/eventbuilder.test.ts
+++ b/packages/utils/test/eventbuilder.test.ts
@@ -30,7 +30,7 @@ describe('eventFromUnknownInput', () => {
test('object with name prop', () => {
const event = eventFromUnknownInput(fakeClient, stackParser, { foo: { bar: 'baz' }, name: 'BadType' });
- expect(event.exception?.values?.[0].value).toBe("'BadType' captured as exception");
+ expect(event.exception?.values?.[0]?.value).toBe("'BadType' captured as exception");
expect(event.exception?.values?.[0]).toEqual(
expect.objectContaining({
@@ -46,7 +46,7 @@ describe('eventFromUnknownInput', () => {
test('object with name and message props', () => {
const event = eventFromUnknownInput(fakeClient, stackParser, { message: 'went wrong', name: 'BadType' });
- expect(event.exception?.values?.[0].value).toBe("'BadType' captured as exception with message 'went wrong'");
+ expect(event.exception?.values?.[0]?.value).toBe("'BadType' captured as exception with message 'went wrong'");
expect(event.exception?.values?.[0]).toEqual(
expect.objectContaining({
@@ -150,6 +150,6 @@ describe('eventFromUnknownInput', () => {
test('passing client directly', () => {
const event = eventFromUnknownInput(fakeClient, stackParser, { foo: { bar: 'baz' }, prop: 1 });
- expect(event.exception?.values?.[0].value).toBe('Object captured as exception with keys: foo, prop');
+ expect(event.exception?.values?.[0]?.value).toBe('Object captured as exception with keys: foo, prop');
});
});
diff --git a/packages/utils/test/misc.test.ts b/packages/utils/test/misc.test.ts
index c1eb978dcdbe..14f3e88c0f0b 100644
--- a/packages/utils/test/misc.test.ts
+++ b/packages/utils/test/misc.test.ts
@@ -215,18 +215,18 @@ describe('addExceptionMechanism', () => {
addExceptionMechanism(event);
- expect(event.exception.values[0].mechanism).toEqual(defaultMechanism);
+ expect(event.exception.values[0]?.mechanism).toEqual(defaultMechanism);
});
it('prefers current values to defaults', () => {
const event = { ...baseEvent };
const nonDefaultMechanism = { type: 'instrument', handled: false };
- event.exception.values[0].mechanism = nonDefaultMechanism;
+ event.exception.values[0]!.mechanism = nonDefaultMechanism;
addExceptionMechanism(event);
- expect(event.exception.values[0].mechanism).toEqual(nonDefaultMechanism);
+ expect(event.exception.values[0]?.mechanism).toEqual(nonDefaultMechanism);
});
it('prefers incoming values to current values', () => {
@@ -234,12 +234,12 @@ describe('addExceptionMechanism', () => {
const currentMechanism = { type: 'instrument', handled: false };
const newMechanism = { handled: true, synthetic: true };
- event.exception.values[0].mechanism = currentMechanism;
+ event.exception.values[0]!.mechanism = currentMechanism;
addExceptionMechanism(event, newMechanism);
// the new `handled` value took precedence
- expect(event.exception.values[0].mechanism).toEqual({ type: 'instrument', handled: true, synthetic: true });
+ expect(event.exception.values[0]?.mechanism).toEqual({ type: 'instrument', handled: true, synthetic: true });
});
it('merges data values', () => {
@@ -247,11 +247,11 @@ describe('addExceptionMechanism', () => {
const currentMechanism = { ...defaultMechanism, data: { function: 'addEventListener' } };
const newMechanism = { data: { handler: 'organizeShoes', target: 'closet' } };
- event.exception.values[0].mechanism = currentMechanism;
+ event.exception.values[0]!.mechanism = currentMechanism;
addExceptionMechanism(event, newMechanism);
- expect(event.exception.values[0].mechanism.data).toEqual({
+ expect(event.exception.values[0]?.mechanism.data).toEqual({
function: 'addEventListener',
handler: 'organizeShoes',
target: 'closet',
diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts
index c0bbe0298ba2..5a2414d52e43 100644
--- a/packages/utils/test/normalize.test.ts
+++ b/packages/utils/test/normalize.test.ts
@@ -178,8 +178,8 @@ describe('normalize()', () => {
name: 'Alice',
children: [{ name: 'Bob' }, { name: 'Eve' }],
} as any;
- obj.children[0].self = obj.children[0];
- obj.children[1].self = obj.children[1];
+ obj.children[0]!.self = obj.children[0];
+ obj.children[1]!.self = obj.children[1];
expect(normalize(obj)).toEqual({
name: 'Alice',
children: [
@@ -269,7 +269,7 @@ describe('normalize()', () => {
},
circular,
];
- circular.qux = circular.bar[0].baz;
+ circular.qux = circular.bar[0]?.baz;
const normalized = normalize(circular);
expect(normalized).toEqual({
@@ -283,9 +283,9 @@ describe('normalize()', () => {
qux: '[Circular ~]',
});
- expect(circular.bar[0].baz).toBe(circular);
+ expect(circular.bar[0]?.baz).toBe(circular);
expect(circular.bar[1]).toBe(circular);
- expect(circular.qux).toBe(circular.bar[0].baz);
+ expect(circular.qux).toBe(circular.bar[0]?.baz);
expect(normalized).not.toBe(circular);
});
diff --git a/packages/utils/test/object.test.ts b/packages/utils/test/object.test.ts
index 6fbb69f0e2b4..2ba5a6c58fa3 100644
--- a/packages/utils/test/object.test.ts
+++ b/packages/utils/test/object.test.ts
@@ -286,7 +286,7 @@ describe('dropUndefinedKeys()', () => {
// Returns new references within objects
expect(chicken === droppedChicken.lays[0]).toBe(false);
- expect(egg === droppedChicken.lays[0].lays).toBe(false);
+ expect(egg === droppedChicken.lays[0]?.lays).toBe(false);
// Keeps circular reference
expect(droppedChicken.lays[0] === droppedChicken).toBe(true);
diff --git a/packages/utils/test/stacktrace.test.ts b/packages/utils/test/stacktrace.test.ts
index 7e5251d0dd9c..f1ef0454a71a 100644
--- a/packages/utils/test/stacktrace.test.ts
+++ b/packages/utils/test/stacktrace.test.ts
@@ -14,8 +14,8 @@ describe('Stacktrace', () => {
const frames = stripSentryFramesAndReverse(stack);
expect(frames.length).toBe(2);
- expect(frames[0].function).toBe('bar');
- expect(frames[1].function).toBe('foo');
+ expect(frames[0]?.function).toBe('bar');
+ expect(frames[1]?.function).toBe('foo');
});
it('reserved captureMessage', () => {
@@ -29,8 +29,8 @@ describe('Stacktrace', () => {
const frames = stripSentryFramesAndReverse(stack);
expect(frames.length).toBe(2);
- expect(frames[0].function).toBe('bar');
- expect(frames[1].function).toBe('foo');
+ expect(frames[0]?.function).toBe('bar');
+ expect(frames[1]?.function).toBe('foo');
});
it('remove two occurences if they are present', () => {
@@ -44,8 +44,8 @@ describe('Stacktrace', () => {
const exceptionFrames = stripSentryFramesAndReverse(exceptionStack);
expect(exceptionFrames.length).toBe(2);
- expect(exceptionFrames[0].function).toBe('bar');
- expect(exceptionFrames[1].function).toBe('foo');
+ expect(exceptionFrames[0]?.function).toBe('bar');
+ expect(exceptionFrames[1]?.function).toBe('foo');
const messageStack = [
{ colno: 1, lineno: 4, filename: 'anything.js', function: 'captureMessage' },
@@ -57,8 +57,8 @@ describe('Stacktrace', () => {
const messageFrames = stripSentryFramesAndReverse(messageStack);
expect(messageFrames.length).toBe(2);
- expect(messageFrames[0].function).toBe('bar');
- expect(messageFrames[1].function).toBe('foo');
+ expect(messageFrames[0]?.function).toBe('bar');
+ expect(messageFrames[1]?.function).toBe('foo');
});
});
@@ -74,8 +74,8 @@ describe('Stacktrace', () => {
const frames = stripSentryFramesAndReverse(stack);
expect(frames.length).toBe(2);
- expect(frames[0].function).toBe('bar');
- expect(frames[1].function).toBe('foo');
+ expect(frames[0]?.function).toBe('bar');
+ expect(frames[1]?.function).toBe('foo');
});
});
@@ -92,8 +92,8 @@ describe('Stacktrace', () => {
const frames = stripSentryFramesAndReverse(stack);
expect(frames.length).toBe(2);
- expect(frames[0].function).toBe('bar');
- expect(frames[1].function).toBe('foo');
+ expect(frames[0]?.function).toBe('bar');
+ expect(frames[1]?.function).toBe('foo');
});
it('applies frames limit after the stripping, not before', () => {
@@ -111,8 +111,8 @@ describe('Stacktrace', () => {
expect(frames.length).toBe(50);
// Frames are named 0-54, thus after reversal and trimming, we should have frames 54-5, 50 in total.
- expect(frames[0].function).toBe('54');
- expect(frames[49].function).toBe('5');
+ expect(frames[0]?.function).toBe('54');
+ expect(frames[49]?.function).toBe('5');
});
});
});
diff --git a/packages/utils/test/tsconfig.json b/packages/utils/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/utils/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/vercel-edge/test/tsconfig.json b/packages/vercel-edge/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/vercel-edge/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/vercel-edge/test/wintercg-fetch.test.ts b/packages/vercel-edge/test/wintercg-fetch.test.ts
index 79678046d02d..ae830ed9c1e8 100644
--- a/packages/vercel-edge/test/wintercg-fetch.test.ts
+++ b/packages/vercel-edge/test/wintercg-fetch.test.ts
@@ -45,7 +45,7 @@ describe('WinterCGFetch instrumentation', () => {
integration.setupOnce!();
integration.setup!(client);
- const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0];
+ const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]!;
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startHandlerData: HandlerDataFetch = {
@@ -63,7 +63,7 @@ describe('WinterCGFetch instrumentation', () => {
'auto.http.wintercg_fetch',
);
- const [, shouldCreateSpan, shouldAttachTraceData] = instrumentFetchRequestSpy.mock.calls[0];
+ const [, shouldCreateSpan, shouldAttachTraceData] = instrumentFetchRequestSpy.mock.calls[0]!;
expect(shouldAttachTraceData('http://my-website.com/')).toBe(true);
expect(shouldAttachTraceData('https://www.3rd-party-website.at/')).toBe(false);
@@ -79,7 +79,7 @@ describe('WinterCGFetch instrumentation', () => {
integration.setupOnce!();
// integration.setup!(client) is not called!
- const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0];
+ const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]!;
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startHandlerData: HandlerDataFetch = {
@@ -99,7 +99,7 @@ describe('WinterCGFetch instrumentation', () => {
integration.setupOnce!();
integration.setup!(client);
- const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0];
+ const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]!;
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startHandlerData: HandlerDataFetch = {
@@ -123,7 +123,7 @@ describe('WinterCGFetch instrumentation', () => {
integration.setupOnce!();
integration.setup!(client);
- const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0];
+ const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]!;
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startHandlerData: HandlerDataFetch = {
@@ -133,7 +133,7 @@ describe('WinterCGFetch instrumentation', () => {
};
fetchInstrumentationHandlerCallback(startHandlerData);
- const [, shouldCreateSpan] = instrumentFetchRequestSpy.mock.calls[0];
+ const [, shouldCreateSpan] = instrumentFetchRequestSpy.mock.calls[0]!;
expect(shouldCreateSpan('http://only-acceptable-url.com/')).toBe(true);
expect(shouldCreateSpan('http://my-website.com/')).toBe(false);
@@ -147,7 +147,7 @@ describe('WinterCGFetch instrumentation', () => {
integration.setupOnce!();
integration.setup!(client);
- const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0];
+ const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]!;
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startTimestamp = Date.now();
@@ -189,7 +189,7 @@ describe('WinterCGFetch instrumentation', () => {
integration.setupOnce!();
integration.setup!(client);
- const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0];
+ const [fetchInstrumentationHandlerCallback] = addFetchInstrumentationHandlerSpy.mock.calls[0]!;
expect(fetchInstrumentationHandlerCallback).toBeDefined();
const startTimestamp = Date.now();
diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts
index b1d7163e48d1..e54c71eb550f 100644
--- a/packages/vue/src/router.ts
+++ b/packages/vue/src/router.ts
@@ -85,7 +85,8 @@ export function instrumentVueRouter(
transactionSource = 'custom';
} else if (to.matched.length > 0) {
const lastIndex = to.matched.length - 1;
- spanName = to.matched[lastIndex].path;
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ spanName = to.matched[lastIndex]!.path;
transactionSource = 'route';
}
diff --git a/packages/vue/test/errorHandler.test.ts b/packages/vue/test/errorHandler.test.ts
index 3245fa80a356..e6ac911c533c 100644
--- a/packages/vue/test/errorHandler.test.ts
+++ b/packages/vue/test/errorHandler.test.ts
@@ -390,7 +390,7 @@ const testHarness = ({
const captureExceptionSpy = client.captureException;
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
const error = captureExceptionSpy.mock.calls[0][0];
- const contexts = captureExceptionSpy.mock.calls[0][1].captureContext.contexts;
+ const contexts = captureExceptionSpy.mock.calls[0][1]?.captureContext.contexts;
expect(error).toBeInstanceOf(DummyError);
diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts
index e835855ebd7a..8c7ca7c73e93 100644
--- a/packages/vue/test/router.test.ts
+++ b/packages/vue/test/router.test.ts
@@ -82,7 +82,7 @@ describe('instrumentVueRouter()', () => {
// check
expect(mockVueRouter.onError).toHaveBeenCalledTimes(1);
- const onErrorCallback = mockVueRouter.onError.mock.calls[0][0];
+ const onErrorCallback = mockVueRouter.onError.mock.calls[0]![0]!;
const testError = new Error();
onErrorCallback(testError);
@@ -108,10 +108,10 @@ describe('instrumentVueRouter()', () => {
// check
expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1);
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
- const from = testRoutes[fromKey];
- const to = testRoutes[toKey];
+ const from = testRoutes[fromKey]!;
+ const to = testRoutes[toKey]!;
beforeEachCallback(to, from, mockNext);
expect(mockStartSpan).toHaveBeenCalledTimes(1);
@@ -158,10 +158,10 @@ describe('instrumentVueRouter()', () => {
// no span is started for page load
expect(mockStartSpan).not.toHaveBeenCalled();
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
- const from = testRoutes[fromKey];
- const to = testRoutes[toKey];
+ const from = testRoutes[fromKey]!;
+ const to = testRoutes[toKey]!;
beforeEachCallback(to, from, mockNext);
expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1);
@@ -186,10 +186,10 @@ describe('instrumentVueRouter()', () => {
);
// check
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
- const from = testRoutes.normalRoute1;
- const to = testRoutes.namedRoute;
+ const from = testRoutes.normalRoute1!;
+ const to = testRoutes.namedRoute!;
beforeEachCallback(to, from, mockNext);
// first startTx call happens when the instrumentation is initialized (for pageloads)
@@ -213,10 +213,10 @@ describe('instrumentVueRouter()', () => {
);
// check
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
- const from = testRoutes.normalRoute1;
- const to = testRoutes.namedRoute;
+ const from = testRoutes.normalRoute1!;
+ const to = testRoutes.namedRoute!;
beforeEachCallback(to, from, mockNext);
// first startTx call happens when the instrumentation is initialized (for pageloads)
@@ -268,10 +268,10 @@ describe('instrumentVueRouter()', () => {
},
});
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
- const to = testRoutes['normalRoute1'];
- const from = testRoutes['initialPageloadRoute'];
+ const to = testRoutes['normalRoute1']!;
+ const from = testRoutes['initialPageloadRoute']!;
beforeEachCallback(to, from, mockNext);
@@ -304,10 +304,10 @@ describe('instrumentVueRouter()', () => {
mockStartSpan,
);
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
- const from = testRoutes['initialPageloadRoute'];
- const to = testRoutes['normalRoute1'];
+ const from = testRoutes['initialPageloadRoute']!;
+ const to = testRoutes['normalRoute1']!;
beforeEachCallback(to, from, mockNext);
@@ -346,8 +346,8 @@ describe('instrumentVueRouter()', () => {
// check
expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1);
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
- beforeEachCallback(testRoutes['normalRoute1'], testRoutes['initialPageloadRoute'], mockNext);
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
+ beforeEachCallback(testRoutes['normalRoute1']!, testRoutes['initialPageloadRoute']!, mockNext);
expect(mockRootSpan.updateName).toHaveBeenCalledTimes(expectedCallsAmount);
expect(mockStartSpan).not.toHaveBeenCalled();
@@ -370,8 +370,8 @@ describe('instrumentVueRouter()', () => {
// check
expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1);
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
- beforeEachCallback(testRoutes['normalRoute2'], testRoutes['normalRoute1'], mockNext);
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
+ beforeEachCallback(testRoutes['normalRoute2']!, testRoutes['normalRoute1']!, mockNext);
expect(mockStartSpan).toHaveBeenCalledTimes(expectedCallsAmount);
},
@@ -385,10 +385,10 @@ describe('instrumentVueRouter()', () => {
mockStartSpan,
);
- const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0];
+ const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0]![0]!;
- const from = testRoutes.normalRoute1;
- const to = testRoutes.namedRoute;
+ const from = testRoutes.normalRoute1!;
+ const to = testRoutes.namedRoute!;
beforeEachCallback(to, from, undefined);
// first startTx call happens when the instrumentation is initialized (for pageloads)
diff --git a/packages/vue/test/tsconfig.json b/packages/vue/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/vue/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts
index c3cf09fbbcd8..88eb1915ce06 100644
--- a/packages/wasm/src/index.ts
+++ b/packages/wasm/src/index.ts
@@ -45,8 +45,10 @@ function patchFrames(frames: Array): boolean {
if (!frame.filename) {
return;
}
- const match = frame.filename.match(/^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/);
- if (match !== null) {
+ const match = frame.filename.match(/^(.*?):wasm-function\[\d+\]:(0x[a-fA-F0-9]+)$/) as
+ | null
+ | [string, string, string];
+ if (match) {
const index = getImage(match[1]);
if (index >= 0) {
frame.instruction_addr = match[2];
diff --git a/packages/wasm/src/registry.ts b/packages/wasm/src/registry.ts
index 2005c840b630..989022901709 100644
--- a/packages/wasm/src/registry.ts
+++ b/packages/wasm/src/registry.ts
@@ -18,16 +18,18 @@ export function getModuleInfo(module: WebAssembly.Module): ModuleInfo {
let buildId = null;
let debugFile = null;
- if (buildIds.length > 0) {
- const firstBuildId = new Uint8Array(buildIds[0]);
+ const buildId0 = buildIds[0];
+ if (buildId0) {
+ const firstBuildId = new Uint8Array(buildId0);
buildId = Array.from(firstBuildId).reduce((acc, x) => {
return acc + x.toString(16).padStart(2, '0');
}, '');
}
const externalDebugInfo = WebAssembly.Module.customSections(module, 'external_debug_info');
- if (externalDebugInfo.length > 0) {
- const firstExternalDebugInfo = new Uint8Array(externalDebugInfo[0]);
+ const externalDebugInfo0 = externalDebugInfo[0];
+ if (externalDebugInfo0) {
+ const firstExternalDebugInfo = new Uint8Array(externalDebugInfo0);
const decoder = new TextDecoder('utf-8');
debugFile = decoder.decode(firstExternalDebugInfo);
}
diff --git a/scripts/prepack.ts b/scripts/prepack.ts
index 43febdcde4ee..fd3d12e52d60 100644
--- a/scripts/prepack.ts
+++ b/scripts/prepack.ts
@@ -78,6 +78,10 @@ function rewriteConditionalExportEntryPoint(
key: string,
): void {
const exportsField = exportsObject[key];
+ if (!exportsField) {
+ return;
+ }
+
if (typeof exportsField === 'string') {
exportsObject[key] = exportsField.replace(`${buildDir}/`, '');
return;
diff --git a/yarn.lock b/yarn.lock
index e83c8fe9f310..897d9682512e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6844,17 +6844,17 @@
dependencies:
web-streams-polyfill "^3.1.1"
-"@rollup/plugin-commonjs@24.0.0":
- version "24.0.0"
- resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c"
- integrity sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==
+"@rollup/plugin-commonjs@26.0.1":
+ version "26.0.1"
+ resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-26.0.1.tgz#16d4d6e54fa63021249a292b50f27c0b0f1a30d8"
+ integrity sha512-UnsKoZK6/aGIH6AdkptXhNvhaqftcjq3zZdT+LY5Ftms6JR06nADcDsYp5hTU9E2lbJUEOhdlY5J4DNTneM+jQ==
dependencies:
"@rollup/pluginutils" "^5.0.1"
commondir "^1.0.1"
estree-walker "^2.0.2"
- glob "^8.0.3"
+ glob "^10.4.1"
is-reference "1.2.1"
- magic-string "^0.27.0"
+ magic-string "^0.30.3"
"@rollup/plugin-commonjs@^25.0.7":
version "25.0.7"
@@ -17758,7 +17758,7 @@ glob@^10.2.2:
minipass "^5.0.0 || ^6.0.2"
path-scurry "^1.10.0"
-glob@^10.3.10:
+glob@^10.3.10, glob@^10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2"
integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==
@@ -21690,7 +21690,7 @@ magic-string@0.26.2:
dependencies:
sourcemap-codec "^1.4.8"
-magic-string@0.27.0, magic-string@^0.27.0:
+magic-string@0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==
@@ -24190,6 +24190,14 @@ opentelemetry-instrumentation-fetch-node@1.2.0:
"@opentelemetry/instrumentation" "^0.43.0"
"@opentelemetry/semantic-conventions" "^1.17.0"
+opentelemetry-instrumentation-remix@0.7.0:
+ version "0.7.0"
+ resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-remix/-/opentelemetry-instrumentation-remix-0.7.0.tgz#ea3ac4d6da69300de1417c938eade2d029c5cd21"
+ integrity sha512-dF7IdcLkN2xIUATaa2X4ahb/Plk/c2wPdOz90MCVgFHuQZvGtzP9DwBpxXEzs6dz4f57ZzJsHpwJvAXHCSJrbg==
+ dependencies:
+ "@opentelemetry/instrumentation" "^0.43.0"
+ "@opentelemetry/semantic-conventions" "^1.17.0"
+
optional-require@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.0.3.tgz#275b8e9df1dc6a17ad155369c2422a440f89cb07"
@@ -25565,7 +25573,7 @@ postcss@^8.2.14, postcss@^8.4.7, postcss@^8.4.8:
picocolors "^1.0.0"
source-map-js "^1.1.0"
-postcss@^8.4.23, postcss@^8.4.36:
+postcss@^8.4.23, postcss@^8.4.36, postcss@^8.4.38:
version "8.4.38"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
@@ -30634,6 +30642,17 @@ vite@^5.0.10:
optionalDependencies:
fsevents "~2.3.3"
+vite@^5.2.11:
+ version "5.2.11"
+ resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.11.tgz#726ec05555431735853417c3c0bfb36003ca0cbd"
+ integrity sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==
+ dependencies:
+ esbuild "^0.20.1"
+ postcss "^8.4.38"
+ rollup "^4.13.0"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
vitefu@^0.2.2, vitefu@^0.2.5:
version "0.2.5"
resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969"