@sentry/remix
Version:
Official Sentry SDK for Remix
327 lines (279 loc) • 11 kB
JavaScript
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
const opentelemetry = require('@opentelemetry/api');
const instrumentation = require('@opentelemetry/instrumentation');
const semanticConventions = require('@opentelemetry/semantic-conventions');
const core = require('@sentry/core');
const RemixSemanticAttributes = {
MATCH_PARAMS: 'match.params',
MATCH_ROUTE_ID: 'match.route.id',
};
const VERSION = core.SDK_VERSION;
const DEFAULT_CONFIG = {
actionFormDataAttributes: {
_action: 'actionType',
},
};
class RemixInstrumentation extends instrumentation.InstrumentationBase {
constructor(config = {}) {
super('RemixInstrumentation', VERSION, Object.assign({}, DEFAULT_CONFIG, config));
}
getConfig() {
return this._config;
}
setConfig(config = {}) {
this._config = Object.assign({}, DEFAULT_CONFIG, config);
}
// eslint-disable-next-line @typescript-eslint/naming-convention
init() {
const remixRunServerRuntimeRouteMatchingFile = new instrumentation.InstrumentationNodeModuleFile(
'@remix-run/server-runtime/dist/routeMatching.js',
['2.x'],
(moduleExports) => {
// createRequestHandler
if (instrumentation.isWrapped(moduleExports['matchServerRoutes'])) {
this._unwrap(moduleExports, 'matchServerRoutes');
}
this._wrap(moduleExports, 'matchServerRoutes', this._patchMatchServerRoutes());
return moduleExports;
},
(moduleExports) => {
this._unwrap(moduleExports, 'matchServerRoutes');
},
);
const remixRunServerRuntimeData_File = new instrumentation.InstrumentationNodeModuleFile(
'@remix-run/server-runtime/dist/data.js',
['2.9.0 - 2.x'],
(moduleExports) => {
// callRouteLoader
if (instrumentation.isWrapped(moduleExports['callRouteLoader'])) {
this._unwrap(moduleExports, 'callRouteLoader');
}
this._wrap(moduleExports, 'callRouteLoader', this._patchCallRouteLoader());
// callRouteAction
if (instrumentation.isWrapped(moduleExports['callRouteAction'])) {
this._unwrap(moduleExports, 'callRouteAction');
}
this._wrap(moduleExports, 'callRouteAction', this._patchCallRouteAction());
return moduleExports;
},
(moduleExports) => {
this._unwrap(moduleExports, 'callRouteLoader');
this._unwrap(moduleExports, 'callRouteAction');
},
);
/*
* In Remix 2.9.0, the `callXXLoaderRR` functions were renamed to `callXXLoader`.
*/
const remixRunServerRuntimeDataPre_2_9_File = new instrumentation.InstrumentationNodeModuleFile(
'@remix-run/server-runtime/dist/data.js',
['2.0.0 - 2.8.x'],
(
moduleExports
,
) => {
// callRouteLoader
if (instrumentation.isWrapped(moduleExports['callRouteLoaderRR'])) {
this._unwrap(moduleExports, 'callRouteLoaderRR');
}
this._wrap(moduleExports, 'callRouteLoaderRR', this._patchCallRouteLoader());
// callRouteAction
if (instrumentation.isWrapped(moduleExports['callRouteActionRR'])) {
this._unwrap(moduleExports, 'callRouteActionRR');
}
this._wrap(moduleExports, 'callRouteActionRR', this._patchCallRouteAction());
return moduleExports;
},
(
moduleExports
,
) => {
this._unwrap(moduleExports, 'callRouteLoaderRR');
this._unwrap(moduleExports, 'callRouteActionRR');
},
);
const remixRunServerRuntimeModule = new instrumentation.InstrumentationNodeModuleDefinition(
'@remix-run/server-runtime',
['2.x'],
(moduleExports) => {
// createRequestHandler
if (instrumentation.isWrapped(moduleExports['createRequestHandler'])) {
this._unwrap(moduleExports, 'createRequestHandler');
}
this._wrap(moduleExports, 'createRequestHandler', this._patchCreateRequestHandler());
return moduleExports;
},
(moduleExports) => {
this._unwrap(moduleExports, 'createRequestHandler');
},
[remixRunServerRuntimeRouteMatchingFile, remixRunServerRuntimeData_File, remixRunServerRuntimeDataPre_2_9_File],
);
return remixRunServerRuntimeModule;
}
_patchMatchServerRoutes() {
return function matchServerRoutes(original) {
return function patchMatchServerRoutes(
...args
) {
const result = original.apply(this, args) ;
const span = opentelemetry.default.trace.getSpan(opentelemetry.default.context.active());
const route = (result || []).slice(-1)[0]?.route;
const routePath = route?.path;
if (span && routePath) {
span.setAttribute(semanticConventions.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;
};
};
}
_patchCreateRequestHandler() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const plugin = this;
return function createRequestHandler(original) {
return function patchCreateRequestHandler(
...args
) {
const originalRequestHandler = original.apply(this, args);
return (request, loadContext) => {
const span = plugin.tracer.startSpan(
'remix.request',
{
attributes: { [semanticConventions.SemanticAttributes.CODE_FUNCTION]: 'requestHandler' },
},
opentelemetry.default.context.active(),
);
addRequestAttributesToSpan(span, request);
const originalResponsePromise = opentelemetry.default.context.with(
opentelemetry.default.trace.setSpan(opentelemetry.default.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();
});
};
};
};
}
_patchCallRouteLoader() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const plugin = this;
return function callRouteLoader(original) {
return function patchCallRouteLoader( ...args) {
const [params] = args;
const span = plugin.tracer.startSpan(
`LOADER ${params.routeId}`,
{ attributes: { [semanticConventions.SemanticAttributes.CODE_FUNCTION]: 'loader' } },
opentelemetry.default.context.active(),
);
addRequestAttributesToSpan(span, params.request);
addMatchAttributesToSpan(span, { routeId: params.routeId, params: params.params });
return opentelemetry.default.context.with(opentelemetry.default.trace.setSpan(opentelemetry.default.context.active(), span), () => {
const originalResponsePromise = original.apply(this, args);
return originalResponsePromise
.then(response => {
addResponseAttributesToSpan(span, response);
return response;
})
.catch(error => {
plugin._addErrorToSpan(span, error);
throw error;
})
.finally(() => {
span.end();
});
});
};
};
}
_patchCallRouteAction() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const plugin = this;
return function callRouteAction(original) {
return async function patchCallRouteAction( ...args) {
const [params] = args;
const clonedRequest = params.request.clone();
const span = plugin.tracer.startSpan(
`ACTION ${params.routeId}`,
{ attributes: { [semanticConventions.SemanticAttributes.CODE_FUNCTION]: 'action' } },
opentelemetry.default.context.active(),
);
addRequestAttributesToSpan(span, clonedRequest);
addMatchAttributesToSpan(span, { routeId: params.routeId, params: params.params });
return opentelemetry.default.context.with(
opentelemetry.default.trace.setSpan(opentelemetry.default.context.active(), span),
async () => {
const originalResponsePromise = 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, key) => {
if (actionFormAttributes?.[key] && 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();
});
},
);
};
};
}
_addErrorToSpan(span, error) {
addErrorEventToSpan(span, error);
}
}
const addRequestAttributesToSpan = (span, request) => {
span.setAttributes({
[semanticConventions.SemanticAttributes.HTTP_METHOD]: request.method,
[semanticConventions.SemanticAttributes.HTTP_URL]: request.url,
});
};
const addMatchAttributesToSpan = (span, match) => {
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, response) => {
if (response) {
span.setAttributes({
[semanticConventions.SemanticAttributes.HTTP_STATUS_CODE]: response.status,
});
}
};
const addErrorEventToSpan = (span, error) => {
span.recordException(error);
span.setStatus({ code: opentelemetry.SpanStatusCode.ERROR, message: error.message });
};
exports.RemixInstrumentation = RemixInstrumentation;
//# sourceMappingURL=instrumentation.js.map