From db16044bce1bc3b045268672ecb2edd5debb09dc Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 28 Apr 2025 15:26:40 +0100 Subject: [PATCH 1/8] feat(remix): Vendor in `opentelemetry-instrumentation-remix` --- packages/remix/package.json | 3 +- .../src/server/integrations/opentelemetry.ts | 2 +- packages/remix/src/vendor/instrumentation.ts | 443 ++++++++++++++++++ yarn.lock | 44 +- 4 files changed, 457 insertions(+), 35 deletions(-) create mode 100644 packages/remix/src/vendor/instrumentation.ts diff --git a/packages/remix/package.json b/packages/remix/package.json index 122b1dfdf329..f2c8d2c4f2a2 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -65,6 +65,8 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", + "@opentelemetry/instrumentation": "^0.57.2", + "@opentelemetry/semantic-conventions": "^1.30.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.43.0", "@sentry/core": "9.18.0", @@ -72,7 +74,6 @@ "@sentry/opentelemetry": "9.18.0", "@sentry/react": "9.18.0", "glob": "^10.3.4", - "opentelemetry-instrumentation-remix": "0.8.0", "yargs": "^17.6.0" }, "devDependencies": { diff --git a/packages/remix/src/server/integrations/opentelemetry.ts b/packages/remix/src/server/integrations/opentelemetry.ts index 42654201da18..b4ad1d28bb6a 100644 --- a/packages/remix/src/server/integrations/opentelemetry.ts +++ b/packages/remix/src/server/integrations/opentelemetry.ts @@ -1,8 +1,8 @@ import type { Client, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { generateInstrumentOnce, getClient, spanToJSON } from '@sentry/node'; -import { RemixInstrumentation } from 'opentelemetry-instrumentation-remix'; import type { RemixOptions } from '../../utils/remixOptions'; +import { RemixInstrumentation } from '../../vendor/instrumentation'; const INTEGRATION_NAME = 'Remix'; diff --git a/packages/remix/src/vendor/instrumentation.ts b/packages/remix/src/vendor/instrumentation.ts new file mode 100644 index 000000000000..a646378bb5c1 --- /dev/null +++ b/packages/remix/src/vendor/instrumentation.ts @@ -0,0 +1,443 @@ +/* eslint-disable deprecation/deprecation */ +/* eslint-disable max-lines */ +import type { Span } from '@opentelemetry/api'; +import opentelemetry, { SpanStatusCode } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, +} from '@opentelemetry/instrumentation'; +import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import type { Params } from '@remix-run/router'; +import type * as remixRunServerRuntime from '@remix-run/server-runtime'; +import type * as remixRunServerRuntimeData from '@remix-run/server-runtime/dist/data'; +import type * as remixRunServerRuntimeRouteMatching from '@remix-run/server-runtime/dist/routeMatching'; +import type { RouteMatch } from '@remix-run/server-runtime/dist/routeMatching'; +import type { ServerRoute } from '@remix-run/server-runtime/dist/routes'; + +const RemixSemanticAttributes = { + MATCH_PARAMS: 'match.params', + MATCH_ROUTE_ID: 'match.route.id', +}; + +const VERSION = '__OTEL_REMIX_INSTRUMENTATION_VERSION__'; + +export interface RemixInstrumentationConfig extends InstrumentationConfig { + /** + * Mapping of FormData field to span attribute names. Appends attribute as `formData.${name}`. + * + * Provide `true` value to use the FormData field name as the attribute name, or provide + * a `string` value to map the field name to a custom attribute name. + * + * @default { _action: "actionType" } + */ + actionFormDataAttributes?: Record; + /** + * Whether to emit errors in the form of span attributes, as well as in span exception events. + * Defaults to `false`, meaning that only span exception events are emitted. + */ + legacyErrorAttributes?: boolean; +} + +const DEFAULT_CONFIG: RemixInstrumentationConfig = { + actionFormDataAttributes: { + _action: 'actionType', + }, + legacyErrorAttributes: false, +}; + +/** + * + */ +export class RemixInstrumentation extends InstrumentationBase { + public constructor(config: RemixInstrumentationConfig = {}) { + super('RemixInstrumentation', VERSION, Object.assign({}, DEFAULT_CONFIG, config)); + } + + /** + * + */ + public override getConfig(): RemixInstrumentationConfig { + return this._config; + } + + /** + * + */ + public override setConfig(config: RemixInstrumentationConfig = {}): void { + this._config = Object.assign({}, DEFAULT_CONFIG, config); + } + + /** + * + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + protected override init(): InstrumentationNodeModuleDefinition { + const remixRunServerRuntimeRouteMatchingFile = new InstrumentationNodeModuleFile( + '@remix-run/server-runtime/dist/routeMatching.js', + ['1.6.2 - 2.x'], + (moduleExports: typeof remixRunServerRuntimeRouteMatching) => { + // createRequestHandler + if (isWrapped(moduleExports['matchServerRoutes'])) { + this._unwrap(moduleExports, 'matchServerRoutes'); + } + this._wrap(moduleExports, 'matchServerRoutes', this._patchMatchServerRoutes()); + + return moduleExports; + }, + (moduleExports: typeof remixRunServerRuntimeRouteMatching) => { + this._unwrap(moduleExports, 'matchServerRoutes'); + }, + ); + + const remixRunServerRuntimeData_File = new InstrumentationNodeModuleFile( + '@remix-run/server-runtime/dist/data.js', + ['2.9.0 - 2.x'], + (moduleExports: typeof remixRunServerRuntimeData) => { + // callRouteLoader + if (isWrapped(moduleExports['callRouteLoader'])) { + this._unwrap(moduleExports, 'callRouteLoader'); + } + this._wrap(moduleExports, 'callRouteLoader', this._patchCallRouteLoader()); + + // callRouteAction + if (isWrapped(moduleExports['callRouteAction'])) { + this._unwrap(moduleExports, 'callRouteAction'); + } + this._wrap(moduleExports, 'callRouteAction', this._patchCallRouteAction()); + return moduleExports; + }, + (moduleExports: typeof remixRunServerRuntimeData) => { + this._unwrap(moduleExports, 'callRouteLoader'); + this._unwrap(moduleExports, 'callRouteAction'); + }, + ); + + /* + * In Remix 1.8.0, the callXXLoader functions were renamed to callXXLoaderRR. They were renamed back in 2.9.0. + */ + const remixRunServerRuntimeDataPre_2_9_File = new InstrumentationNodeModuleFile( + '@remix-run/server-runtime/dist/data.js', + ['2.0.0 - 2.8.x'], + ( + moduleExports: typeof remixRunServerRuntimeData & { + callRouteLoaderRR: typeof remixRunServerRuntimeData.callRouteLoader; + callRouteActionRR: typeof remixRunServerRuntimeData.callRouteAction; + }, + ) => { + // callRouteLoader + if (isWrapped(moduleExports['callRouteLoaderRR'])) { + this._unwrap(moduleExports, 'callRouteLoaderRR'); + } + this._wrap(moduleExports, 'callRouteLoaderRR', this._patchCallRouteLoader()); + + // callRouteAction + if (isWrapped(moduleExports['callRouteActionRR'])) { + this._unwrap(moduleExports, 'callRouteActionRR'); + } + this._wrap(moduleExports, 'callRouteActionRR', this._patchCallRouteAction()); + return moduleExports; + }, + ( + moduleExports: typeof remixRunServerRuntimeData & { + callRouteLoaderRR: typeof remixRunServerRuntimeData.callRouteLoader; + callRouteActionRR: typeof remixRunServerRuntimeData.callRouteAction; + }, + ) => { + this._unwrap(moduleExports, 'callRouteLoaderRR'); + this._unwrap(moduleExports, 'callRouteActionRR'); + }, + ); + + const remixRunServerRuntimeModule = new InstrumentationNodeModuleDefinition( + '@remix-run/server-runtime', + ['>=2.*'], + (moduleExports: typeof remixRunServerRuntime) => { + // createRequestHandler + if (isWrapped(moduleExports['createRequestHandler'])) { + this._unwrap(moduleExports, 'createRequestHandler'); + } + this._wrap(moduleExports, 'createRequestHandler', this._patchCreateRequestHandler()); + + return moduleExports; + }, + (moduleExports: typeof remixRunServerRuntime) => { + this._unwrap(moduleExports, 'createRequestHandler'); + }, + [remixRunServerRuntimeRouteMatchingFile, remixRunServerRuntimeData_File, remixRunServerRuntimeDataPre_2_9_File], + ); + + return remixRunServerRuntimeModule; + } + + /** + * + */ + private _patchMatchServerRoutes(): (original: typeof remixRunServerRuntimeRouteMatching.matchServerRoutes) => any { + return function matchServerRoutes(original) { + return function patchMatchServerRoutes( + this: any, + ...args: Parameters + ): RouteMatch[] | null { + const result = original.apply(this, args) as RouteMatch[] | null; + + const span = opentelemetry.trace.getSpan(opentelemetry.context.active()); + + const route = (result || []).slice(-1)[0]?.route; + + const routePath = route?.path; + if (span && routePath) { + span.setAttribute(SemanticAttributes.HTTP_ROUTE, routePath); + span.updateName(`remix.request ${routePath}`); + } + + const routeId = route?.id; + if (span && routeId) { + span.setAttribute(RemixSemanticAttributes.MATCH_ROUTE_ID, routeId); + } + + return result; + }; + }; + } + + /** + * + */ + private _patchCreateRequestHandler(): (original: typeof remixRunServerRuntime.createRequestHandler) => any { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const plugin = this; + return function createRequestHandler(original) { + return function patchCreateRequestHandler( + this: any, + ...args: Parameters + ): remixRunServerRuntime.RequestHandler { + const originalRequestHandler: remixRunServerRuntime.RequestHandler = original.apply(this, args); + + return (request: Request, loadContext?: remixRunServerRuntime.AppLoadContext) => { + const span = plugin.tracer.startSpan( + 'remix.request', + { + attributes: { [SemanticAttributes.CODE_FUNCTION]: 'requestHandler' }, + }, + opentelemetry.context.active(), + ); + addRequestAttributesToSpan(span, request); + + const originalResponsePromise = opentelemetry.context.with( + opentelemetry.trace.setSpan(opentelemetry.context.active(), span), + () => originalRequestHandler(request, loadContext), + ); + return originalResponsePromise + .then(response => { + addResponseAttributesToSpan(span, response); + return response; + }) + .catch(error => { + plugin._addErrorToSpan(span, error); + throw error; + }) + .finally(() => { + span.end(); + }); + }; + }; + }; + } + + /** + * + */ + private _patchCallRouteLoader(): (original: typeof remixRunServerRuntimeData.callRouteLoader) => any { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const plugin = this; + return function callRouteLoader(original) { + return function patchCallRouteLoader(this: any, ...args: Parameters): Promise { + const [params] = args; + + const span = plugin.tracer.startSpan( + `LOADER ${params.routeId}`, + { attributes: { [SemanticAttributes.CODE_FUNCTION]: 'loader' } }, + opentelemetry.context.active(), + ); + + addRequestAttributesToSpan(span, params.request); + addMatchAttributesToSpan(span, { routeId: params.routeId, params: params.params }); + + return opentelemetry.context.with(opentelemetry.trace.setSpan(opentelemetry.context.active(), span), () => { + const originalResponsePromise: Promise = original.apply(this, args); + return originalResponsePromise + .then(response => { + addResponseAttributesToSpan(span, response); + return response; + }) + .catch(error => { + plugin._addErrorToSpan(span, error); + throw error; + }) + .finally(() => { + span.end(); + }); + }); + }; + }; + } + + /** + * + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + private _patchCallRouteLoaderPre_1_7_2(): (original: typeof remixRunServerRuntimeData.callRouteLoader) => any { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const plugin = this; + return function callRouteLoader(original) { + return function patchCallRouteLoader(this: any, ...args: Parameters): Promise { + // Cast as `any` to avoid typescript errors since this is patching an older version + const [params] = args as unknown as any; + + const span = plugin.tracer.startSpan( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + `LOADER ${params.match.route.id}`, + { attributes: { [SemanticAttributes.CODE_FUNCTION]: 'loader' } }, + opentelemetry.context.active(), + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + addRequestAttributesToSpan(span, params.request); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + addMatchAttributesToSpan(span, { routeId: params.match.route.id, params: params.match.params }); + + return opentelemetry.context.with(opentelemetry.trace.setSpan(opentelemetry.context.active(), span), () => { + const originalResponsePromise: Promise = original.apply(this, args); + return originalResponsePromise + .then(response => { + addResponseAttributesToSpan(span, response); + return response; + }) + .catch(error => { + plugin._addErrorToSpan(span, error); + throw error; + }) + .finally(() => { + span.end(); + }); + }); + }; + }; + } + + /** + * + */ + private _patchCallRouteAction(): (original: typeof remixRunServerRuntimeData.callRouteAction) => any { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const plugin = this; + return function callRouteAction(original) { + return async function patchCallRouteAction(this: any, ...args: Parameters): Promise { + const [params] = args; + const clonedRequest = params.request.clone(); + const span = plugin.tracer.startSpan( + `ACTION ${params.routeId}`, + { attributes: { [SemanticAttributes.CODE_FUNCTION]: 'action' } }, + opentelemetry.context.active(), + ); + + addRequestAttributesToSpan(span, clonedRequest); + addMatchAttributesToSpan(span, { routeId: params.routeId, params: params.params }); + + return opentelemetry.context.with( + opentelemetry.trace.setSpan(opentelemetry.context.active(), span), + async () => { + const originalResponsePromise: Promise = original.apply(this, args); + + return originalResponsePromise + .then(async response => { + addResponseAttributesToSpan(span, response); + + try { + const formData = await clonedRequest.formData(); + const { actionFormDataAttributes: actionFormAttributes } = plugin.getConfig(); + + formData.forEach((value: unknown, key: string) => { + if ( + actionFormAttributes?.[key] && + actionFormAttributes[key] !== false && + typeof value === 'string' + ) { + const keyName = actionFormAttributes[key] === true ? key : actionFormAttributes[key]; + span.setAttribute(`formData.${keyName}`, value.toString()); + } + }); + } catch { + // Silently continue on any error. Typically happens because the action body cannot be processed + // into FormData, in which case we should just continue. + } + + return response; + }) + .catch(async error => { + plugin._addErrorToSpan(span, error); + throw error; + }) + .finally(() => { + span.end(); + }); + }, + ); + }; + }; + } + + /** + * + */ + private _addErrorToSpan(span: Span, error: Error): void { + addErrorEventToSpan(span, error); + + if (this.getConfig().legacyErrorAttributes || false) { + addErrorAttributesToSpan(span, error); + } + } +} + +const addRequestAttributesToSpan = (span: Span, request: Request): void => { + span.setAttributes({ + [SemanticAttributes.HTTP_METHOD]: request.method, + [SemanticAttributes.HTTP_URL]: request.url, + }); +}; + +const addMatchAttributesToSpan = (span: Span, match: { routeId: string; params: Params }): void => { + span.setAttributes({ + [RemixSemanticAttributes.MATCH_ROUTE_ID]: match.routeId, + }); + + Object.keys(match.params).forEach(paramName => { + span.setAttribute(`${RemixSemanticAttributes.MATCH_PARAMS}.${paramName}`, match.params[paramName] || '(undefined)'); + }); +}; + +const addResponseAttributesToSpan = (span: Span, response: Response | null): void => { + if (response) { + span.setAttributes({ + [SemanticAttributes.HTTP_STATUS_CODE]: response.status, + }); + } +}; + +const addErrorEventToSpan = (span: Span, error: Error): void => { + span.recordException(error); + span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); +}; + +const addErrorAttributesToSpan = (span: Span, error: Error): void => { + span.setAttribute('error', true); + if (error.message) { + span.setAttribute(SemanticAttributes.EXCEPTION_MESSAGE, error.message); + } + if (error.stack) { + span.setAttribute(SemanticAttributes.EXCEPTION_STACKTRACE, error.stack); + } +}; diff --git a/yarn.lock b/yarn.lock index b8ed2f69b728..7048ecd766a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5427,13 +5427,6 @@ dependencies: "@opentelemetry/api" "^1.3.0" -"@opentelemetry/api-logs@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc" - integrity sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A== - dependencies: - "@opentelemetry/api" "^1.0.0" - "@opentelemetry/api-logs@0.57.2": version "0.57.2" resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz#d4001b9aa3580367b40fe889f3540014f766cc87" @@ -5441,7 +5434,7 @@ dependencies: "@opentelemetry/api" "^1.3.0" -"@opentelemetry/api@1.9.0", "@opentelemetry/api@^1.0.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.0": +"@opentelemetry/api@1.9.0", "@opentelemetry/api@^1.3.0", "@opentelemetry/api@^1.9.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== @@ -5711,18 +5704,6 @@ require-in-the-middle "^7.1.1" shimmer "^1.2.1" -"@opentelemetry/instrumentation@^0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" - integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== - dependencies: - "@opentelemetry/api-logs" "0.52.1" - "@types/shimmer" "^1.0.2" - import-in-the-middle "^1.8.1" - require-in-the-middle "^7.1.1" - semver "^7.5.2" - shimmer "^1.2.1" - "@opentelemetry/propagation-utils@^0.30.16": version "0.30.16" resolved "https://registry.yarnpkg.com/@opentelemetry/propagation-utils/-/propagation-utils-0.30.16.tgz#6715d0225b618ea66cf34cc3800fa3452a8475fa" @@ -5772,10 +5753,15 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== -"@opentelemetry/semantic-conventions@^1.25.1", "@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0": - version "1.33.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.33.0.tgz#ec8ebd2ac768ab366aff94e0e7f27e8ae24fa49f" - integrity sha512-TIpZvE8fiEILFfTlfPnltpBaD3d9/+uQHVCyC3vfdh6WfCXKhNFzoP5RyDDIndfvZC5GrA4pyEDNyjPloJud+w== +"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.30.0": + version "1.30.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz#3a42c4c475482f2ec87c12aad98832dc0087dc9a" + integrity sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw== + +"@opentelemetry/semantic-conventions@^1.29.0": + version "1.32.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz#a15e8f78f32388a7e4655e7f539570e40958ca3f" + integrity sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ== "@opentelemetry/sql-common@^0.40.1": version "0.40.1" @@ -8302,7 +8288,7 @@ "@types/mime" "*" "@types/node" "*" -"@types/shimmer@^1.0.2", "@types/shimmer@^1.2.0": +"@types/shimmer@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.2.0.tgz#9b706af96fa06416828842397a70dfbbf1c14ded" integrity sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg== @@ -22496,14 +22482,6 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -opentelemetry-instrumentation-remix@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/opentelemetry-instrumentation-remix/-/opentelemetry-instrumentation-remix-0.8.0.tgz#cf917395f82b2c995ee46068d85d9fa1c95eb36f" - integrity sha512-2XhIEWfzHeQmxnzv9HzklwkgYMx4NuWwloZuVIwjUb9R28gH5j3rJPqjErTvYSyz0fLbw0gyI+gfYHKHn/v/1Q== - dependencies: - "@opentelemetry/instrumentation" "^0.52.1" - "@opentelemetry/semantic-conventions" "^1.25.1" - optional-require@1.0.x: version "1.0.3" resolved "https://registry.yarnpkg.com/optional-require/-/optional-require-1.0.3.tgz#275b8e9df1dc6a17ad155369c2422a440f89cb07" From 06eda6a0626fa6da2f15f4fdca806c0a3b3b0d8c Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 28 Apr 2025 18:30:48 +0100 Subject: [PATCH 2/8] Tidy up --- packages/remix/src/vendor/instrumentation.ts | 86 ++------------------ 1 file changed, 9 insertions(+), 77 deletions(-) diff --git a/packages/remix/src/vendor/instrumentation.ts b/packages/remix/src/vendor/instrumentation.ts index a646378bb5c1..4e9e19267b82 100644 --- a/packages/remix/src/vendor/instrumentation.ts +++ b/packages/remix/src/vendor/instrumentation.ts @@ -1,5 +1,6 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable max-lines */ +/* eslint-disable jsdoc/require-jsdoc */ import type { Span } from '@opentelemetry/api'; import opentelemetry, { SpanStatusCode } from '@opentelemetry/api'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; @@ -16,13 +17,14 @@ import type * as remixRunServerRuntimeData from '@remix-run/server-runtime/dist/ import type * as remixRunServerRuntimeRouteMatching from '@remix-run/server-runtime/dist/routeMatching'; import type { RouteMatch } from '@remix-run/server-runtime/dist/routeMatching'; import type { ServerRoute } from '@remix-run/server-runtime/dist/routes'; +import { SDK_VERSION } from '@sentry/core'; const RemixSemanticAttributes = { MATCH_PARAMS: 'match.params', MATCH_ROUTE_ID: 'match.route.id', }; -const VERSION = '__OTEL_REMIX_INSTRUMENTATION_VERSION__'; +const VERSION = SDK_VERSION; export interface RemixInstrumentationConfig extends InstrumentationConfig { /** @@ -48,36 +50,24 @@ const DEFAULT_CONFIG: RemixInstrumentationConfig = { legacyErrorAttributes: false, }; -/** - * - */ export class RemixInstrumentation extends InstrumentationBase { public constructor(config: RemixInstrumentationConfig = {}) { super('RemixInstrumentation', VERSION, Object.assign({}, DEFAULT_CONFIG, config)); } - /** - * - */ - public override getConfig(): RemixInstrumentationConfig { + public getConfig(): RemixInstrumentationConfig { return this._config; } - /** - * - */ - public override setConfig(config: RemixInstrumentationConfig = {}): void { + public setConfig(config: RemixInstrumentationConfig = {}): void { this._config = Object.assign({}, DEFAULT_CONFIG, config); } - /** - * - */ // eslint-disable-next-line @typescript-eslint/naming-convention - protected override init(): InstrumentationNodeModuleDefinition { + protected init(): InstrumentationNodeModuleDefinition { const remixRunServerRuntimeRouteMatchingFile = new InstrumentationNodeModuleFile( '@remix-run/server-runtime/dist/routeMatching.js', - ['1.6.2 - 2.x'], + ['2.x'], (moduleExports: typeof remixRunServerRuntimeRouteMatching) => { // createRequestHandler if (isWrapped(moduleExports['matchServerRoutes'])) { @@ -116,7 +106,7 @@ export class RemixInstrumentation extends InstrumentationBase { ); /* - * In Remix 1.8.0, the callXXLoader functions were renamed to callXXLoaderRR. They were renamed back in 2.9.0. + * In Remix 2.9.0, the `callXXLoaderRR` functions were renamed to `callXXLoader`. */ const remixRunServerRuntimeDataPre_2_9_File = new InstrumentationNodeModuleFile( '@remix-run/server-runtime/dist/data.js', @@ -153,7 +143,7 @@ export class RemixInstrumentation extends InstrumentationBase { const remixRunServerRuntimeModule = new InstrumentationNodeModuleDefinition( '@remix-run/server-runtime', - ['>=2.*'], + ['2.x'], (moduleExports: typeof remixRunServerRuntime) => { // createRequestHandler if (isWrapped(moduleExports['createRequestHandler'])) { @@ -172,9 +162,6 @@ export class RemixInstrumentation extends InstrumentationBase { return remixRunServerRuntimeModule; } - /** - * - */ private _patchMatchServerRoutes(): (original: typeof remixRunServerRuntimeRouteMatching.matchServerRoutes) => any { return function matchServerRoutes(original) { return function patchMatchServerRoutes( @@ -203,9 +190,6 @@ export class RemixInstrumentation extends InstrumentationBase { }; } - /** - * - */ private _patchCreateRequestHandler(): (original: typeof remixRunServerRuntime.createRequestHandler) => any { // eslint-disable-next-line @typescript-eslint/no-this-alias const plugin = this; @@ -247,9 +231,6 @@ export class RemixInstrumentation extends InstrumentationBase { }; } - /** - * - */ private _patchCallRouteLoader(): (original: typeof remixRunServerRuntimeData.callRouteLoader) => any { // eslint-disable-next-line @typescript-eslint/no-this-alias const plugin = this; @@ -285,52 +266,6 @@ export class RemixInstrumentation extends InstrumentationBase { }; } - /** - * - */ - // eslint-disable-next-line @typescript-eslint/naming-convention - private _patchCallRouteLoaderPre_1_7_2(): (original: typeof remixRunServerRuntimeData.callRouteLoader) => any { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const plugin = this; - return function callRouteLoader(original) { - return function patchCallRouteLoader(this: any, ...args: Parameters): Promise { - // Cast as `any` to avoid typescript errors since this is patching an older version - const [params] = args as unknown as any; - - const span = plugin.tracer.startSpan( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - `LOADER ${params.match.route.id}`, - { attributes: { [SemanticAttributes.CODE_FUNCTION]: 'loader' } }, - opentelemetry.context.active(), - ); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - addRequestAttributesToSpan(span, params.request); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - addMatchAttributesToSpan(span, { routeId: params.match.route.id, params: params.match.params }); - - return opentelemetry.context.with(opentelemetry.trace.setSpan(opentelemetry.context.active(), span), () => { - const originalResponsePromise: Promise = original.apply(this, args); - return originalResponsePromise - .then(response => { - addResponseAttributesToSpan(span, response); - return response; - }) - .catch(error => { - plugin._addErrorToSpan(span, error); - throw error; - }) - .finally(() => { - span.end(); - }); - }); - }; - }; - } - - /** - * - */ private _patchCallRouteAction(): (original: typeof remixRunServerRuntimeData.callRouteAction) => any { // eslint-disable-next-line @typescript-eslint/no-this-alias const plugin = this; @@ -390,9 +325,6 @@ export class RemixInstrumentation extends InstrumentationBase { }; } - /** - * - */ private _addErrorToSpan(span: Span, error: Error): void { addErrorEventToSpan(span, error); From 8dd8ef1b40215930bf0f13d02ceca22824397352 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 28 Apr 2025 18:36:50 +0100 Subject: [PATCH 3/8] Add license --- packages/remix/src/vendor/instrumentation.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/remix/src/vendor/instrumentation.ts b/packages/remix/src/vendor/instrumentation.ts index 4e9e19267b82..d46e216bdfc9 100644 --- a/packages/remix/src/vendor/instrumentation.ts +++ b/packages/remix/src/vendor/instrumentation.ts @@ -1,6 +1,26 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable max-lines */ /* eslint-disable jsdoc/require-jsdoc */ + +// Vendored and modified from: +// https://github.com/justindsmith/opentelemetry-instrumentations-js/blob/3b1e8c3e566e5cc3389e9c28cafce6a5ebb39600/packages/instrumentation-remix/src/instrumentation.ts + +/* + * Copyright Justin Smith + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import type { Span } from '@opentelemetry/api'; import opentelemetry, { SpanStatusCode } from '@opentelemetry/api'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; From 8fa0d9c524574c4dfbeabbe2cce0b54398a5ad3a Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 28 Apr 2025 21:07:00 +0100 Subject: [PATCH 4/8] Remove `legacyErrorAttributes` --- packages/remix/src/vendor/instrumentation.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/remix/src/vendor/instrumentation.ts b/packages/remix/src/vendor/instrumentation.ts index d46e216bdfc9..317a17da663d 100644 --- a/packages/remix/src/vendor/instrumentation.ts +++ b/packages/remix/src/vendor/instrumentation.ts @@ -56,18 +56,12 @@ export interface RemixInstrumentationConfig extends InstrumentationConfig { * @default { _action: "actionType" } */ actionFormDataAttributes?: Record; - /** - * Whether to emit errors in the form of span attributes, as well as in span exception events. - * Defaults to `false`, meaning that only span exception events are emitted. - */ - legacyErrorAttributes?: boolean; } const DEFAULT_CONFIG: RemixInstrumentationConfig = { actionFormDataAttributes: { _action: 'actionType', }, - legacyErrorAttributes: false, }; export class RemixInstrumentation extends InstrumentationBase { @@ -347,10 +341,6 @@ export class RemixInstrumentation extends InstrumentationBase { private _addErrorToSpan(span: Span, error: Error): void { addErrorEventToSpan(span, error); - - if (this.getConfig().legacyErrorAttributes || false) { - addErrorAttributesToSpan(span, error); - } } } @@ -383,13 +373,3 @@ const addErrorEventToSpan = (span: Span, error: Error): void => { span.recordException(error); span.setStatus({ code: SpanStatusCode.ERROR, message: error.message }); }; - -const addErrorAttributesToSpan = (span: Span, error: Error): void => { - span.setAttribute('error', true); - if (error.message) { - span.setAttribute(SemanticAttributes.EXCEPTION_MESSAGE, error.message); - } - if (error.stack) { - span.setAttribute(SemanticAttributes.EXCEPTION_STACKTRACE, error.stack); - } -}; From f41f20cdb585b86e3beacebe29bf74e6da995cc4 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 28 Apr 2025 21:38:53 +0100 Subject: [PATCH 5/8] Align form data logic with opentelemetry implementation --- packages/remix/src/server/errors.ts | 2 +- packages/remix/src/utils/utils.ts | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/remix/src/server/errors.ts b/packages/remix/src/server/errors.ts index 90359212300d..0e26242a0164 100644 --- a/packages/remix/src/server/errors.ts +++ b/packages/remix/src/server/errors.ts @@ -134,7 +134,7 @@ export async function errorHandleDataFunction( const options = getClient()?.getOptions() as RemixOptions | undefined; if (options?.sendDefaultPii && options.captureActionFormDataKeys) { - await storeFormDataKeys(args, span); + await storeFormDataKeys(args, span, options.captureActionFormDataKeys); } } diff --git a/packages/remix/src/utils/utils.ts b/packages/remix/src/utils/utils.ts index a1d878ac1314..5de831c0f0da 100644 --- a/packages/remix/src/utils/utils.ts +++ b/packages/remix/src/utils/utils.ts @@ -10,7 +10,11 @@ type ServerRouteManifest = ServerBuild['routes']; /** * */ -export async function storeFormDataKeys(args: LoaderFunctionArgs | ActionFunctionArgs, span: Span): Promise { +export async function storeFormDataKeys( + args: LoaderFunctionArgs | ActionFunctionArgs, + span: Span, + formDataKeys?: Record | undefined, +): Promise { try { // We clone the request for Remix be able to read the FormData later. const clonedRequest = args.request.clone(); @@ -21,7 +25,17 @@ export async function storeFormDataKeys(args: LoaderFunctionArgs | ActionFunctio const formData = await clonedRequest.formData(); formData.forEach((value, key) => { - span.setAttribute(`remix.action_form_data.${key}`, typeof value === 'string' ? value : '[non-string value]'); + let attrKey = key; + + if (formDataKeys?.[key]) { + if (formDataKeys[key] === false) { + return; + } else if (typeof value === 'string') { + attrKey = key; + } + + span.setAttribute(`remix.action_form_data.${attrKey}`, typeof value === 'string' ? value : '[non-string value]'); + } }); } catch (e) { DEBUG_BUILD && logger.warn('Failed to read FormData from request', e); From a119ab099e605ba5f50f185c9ede2ac961234ab1 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 29 Apr 2025 15:20:54 +0100 Subject: [PATCH 6/8] Lint --- packages/remix/src/utils/utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/remix/src/utils/utils.ts b/packages/remix/src/utils/utils.ts index 5de831c0f0da..9ac06a842215 100644 --- a/packages/remix/src/utils/utils.ts +++ b/packages/remix/src/utils/utils.ts @@ -34,7 +34,10 @@ export async function storeFormDataKeys( attrKey = key; } - span.setAttribute(`remix.action_form_data.${attrKey}`, typeof value === 'string' ? value : '[non-string value]'); + span.setAttribute( + `remix.action_form_data.${attrKey}`, + typeof value === 'string' ? value : '[non-string value]', + ); } }); } catch (e) { From 4cdf0c17fac5918afffa13f45b9e460e40335c08 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 2 May 2025 16:30:47 +0100 Subject: [PATCH 7/8] Fix FormData alias in manual instrumentation --- packages/remix/src/utils/utils.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/remix/src/utils/utils.ts b/packages/remix/src/utils/utils.ts index 9ac06a842215..62fee4b20d61 100644 --- a/packages/remix/src/utils/utils.ts +++ b/packages/remix/src/utils/utils.ts @@ -28,10 +28,8 @@ export async function storeFormDataKeys( let attrKey = key; if (formDataKeys?.[key]) { - if (formDataKeys[key] === false) { - return; - } else if (typeof value === 'string') { - attrKey = key; + if (typeof formDataKeys[key] === 'string') { + attrKey = formDataKeys[key] as string; } span.setAttribute( From 580f5ffed7392ec7a8edcda06d9ceb2d907a1c2e Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 8 May 2025 19:04:26 +0100 Subject: [PATCH 8/8] Dedupe deps --- yarn.lock | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7048ecd766a6..ebee6dfc068a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5753,12 +5753,7 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz#337fb2bca0453d0726696e745f50064411f646d6" integrity sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA== -"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.30.0": - version "1.30.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.30.0.tgz#3a42c4c475482f2ec87c12aad98832dc0087dc9a" - integrity sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw== - -"@opentelemetry/semantic-conventions@^1.29.0": +"@opentelemetry/semantic-conventions@^1.27.0", "@opentelemetry/semantic-conventions@^1.28.0", "@opentelemetry/semantic-conventions@^1.29.0", "@opentelemetry/semantic-conventions@^1.30.0": version "1.32.0" resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz#a15e8f78f32388a7e4655e7f539570e40958ca3f" integrity sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==