Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
291 lines
9.5 KiB
Text
291 lines
9.5 KiB
Text
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
|
const api = require('@opentelemetry/api');
|
|
const core$1 = require('@opentelemetry/core');
|
|
const instrumentation = require('@opentelemetry/instrumentation');
|
|
const core = require('@sentry/core');
|
|
const opentelemetry = require('@sentry/opentelemetry');
|
|
const diagch = require('diagnostics_channel');
|
|
const nodeVersion = require('../../nodeVersion.js');
|
|
const baggage = require('../../utils/baggage.js');
|
|
|
|
const SENTRY_TRACE_HEADER = 'sentry-trace';
|
|
const SENTRY_BAGGAGE_HEADER = 'baggage';
|
|
|
|
// For baggage, we make sure to merge this into a possibly existing header
|
|
const BAGGAGE_HEADER_REGEX = /baggage: (.*)\r\n/;
|
|
|
|
/**
|
|
* This custom node-fetch instrumentation is used to instrument outgoing fetch requests.
|
|
* It does not emit any spans.
|
|
*
|
|
* The reason this is isolated from the OpenTelemetry instrumentation is that users may overwrite this,
|
|
* which would lead to Sentry not working as expected.
|
|
*
|
|
* This is heavily inspired & adapted from:
|
|
* https://github.com/open-telemetry/opentelemetry-js-contrib/blob/28e209a9da36bc4e1f8c2b0db7360170ed46cb80/plugins/node/instrumentation-undici/src/undici.ts
|
|
*/
|
|
class SentryNodeFetchInstrumentation extends instrumentation.InstrumentationBase {
|
|
// Keep ref to avoid https://github.com/nodejs/node/issues/42170 bug and for
|
|
// unsubscribing.
|
|
|
|
constructor(config = {}) {
|
|
super('@sentry/instrumentation-node-fetch', core.SDK_VERSION, config);
|
|
this._channelSubs = [];
|
|
this._propagationDecisionMap = new core.LRUMap(100);
|
|
this._ignoreOutgoingRequestsMap = new WeakMap();
|
|
}
|
|
|
|
/** No need to instrument files/modules. */
|
|
init() {
|
|
return undefined;
|
|
}
|
|
|
|
/** Disable the instrumentation. */
|
|
disable() {
|
|
super.disable();
|
|
this._channelSubs.forEach(sub => sub.unsubscribe());
|
|
this._channelSubs = [];
|
|
}
|
|
|
|
/** Enable the instrumentation. */
|
|
enable() {
|
|
// "enabled" handling is currently a bit messy with InstrumentationBase.
|
|
// If constructed with `{enabled: false}`, this `.enable()` is still called,
|
|
// and `this.getConfig().enabled !== this.isEnabled()`, creating confusion.
|
|
//
|
|
// For now, this class will setup for instrumenting if `.enable()` is
|
|
// called, but use `this.getConfig().enabled` to determine if
|
|
// instrumentation should be generated. This covers the more likely common
|
|
// case of config being given a construction time, rather than later via
|
|
// `instance.enable()`, `.disable()`, or `.setConfig()` calls.
|
|
super.enable();
|
|
|
|
// This method is called by the super-class constructor before ours is
|
|
// called. So we need to ensure the property is initalized.
|
|
this._channelSubs = this._channelSubs || [];
|
|
|
|
// Avoid to duplicate subscriptions
|
|
if (this._channelSubs.length > 0) {
|
|
return;
|
|
}
|
|
|
|
this._subscribeToChannel('undici:request:create', this._onRequestCreated.bind(this));
|
|
this._subscribeToChannel('undici:request:headers', this._onResponseHeaders.bind(this));
|
|
}
|
|
|
|
/**
|
|
* This method is called when a request is created.
|
|
* You can still mutate the request here before it is sent.
|
|
*/
|
|
_onRequestCreated({ request }) {
|
|
const config = this.getConfig();
|
|
const enabled = config.enabled !== false;
|
|
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
|
|
const shouldIgnore = this._shouldIgnoreOutgoingRequest(request);
|
|
// We store this decisision for later so we do not need to re-evaluate it
|
|
// Additionally, the active context is not correct in _onResponseHeaders, so we need to make sure it is evaluated here
|
|
this._ignoreOutgoingRequestsMap.set(request, shouldIgnore);
|
|
|
|
if (shouldIgnore) {
|
|
return;
|
|
}
|
|
|
|
const url = getAbsoluteUrl(request.origin, request.path);
|
|
|
|
// Manually add the trace headers, if it applies
|
|
// Note: We do not use `propagation.inject()` here, because our propagator relies on an active span
|
|
// Which we do not have in this case
|
|
// The propagator _may_ overwrite this, but this should be fine as it is the same data
|
|
const tracePropagationTargets = core.getClient()?.getOptions().tracePropagationTargets;
|
|
const addedHeaders = opentelemetry.shouldPropagateTraceForUrl(url, tracePropagationTargets, this._propagationDecisionMap)
|
|
? core.getTraceData()
|
|
: undefined;
|
|
|
|
if (!addedHeaders) {
|
|
return;
|
|
}
|
|
|
|
const { 'sentry-trace': sentryTrace, baggage: baggage$1 } = addedHeaders;
|
|
|
|
// We do not want to overwrite existing headers here
|
|
// If the core UndiciInstrumentation is registered, it will already have set the headers
|
|
// We do not want to add any then
|
|
if (Array.isArray(request.headers)) {
|
|
const requestHeaders = request.headers;
|
|
|
|
// We do not want to overwrite existing header here, if it was already set
|
|
if (sentryTrace && !requestHeaders.includes(SENTRY_TRACE_HEADER)) {
|
|
requestHeaders.push(SENTRY_TRACE_HEADER, sentryTrace);
|
|
}
|
|
|
|
// For baggage, we make sure to merge this into a possibly existing header
|
|
const existingBaggagePos = requestHeaders.findIndex(header => header === SENTRY_BAGGAGE_HEADER);
|
|
if (baggage$1 && existingBaggagePos === -1) {
|
|
requestHeaders.push(SENTRY_BAGGAGE_HEADER, baggage$1);
|
|
} else if (baggage$1) {
|
|
const existingBaggage = requestHeaders[existingBaggagePos + 1];
|
|
const merged = baggage.mergeBaggageHeaders(existingBaggage, baggage$1);
|
|
if (merged) {
|
|
requestHeaders[existingBaggagePos + 1] = merged;
|
|
}
|
|
}
|
|
} else {
|
|
const requestHeaders = request.headers;
|
|
// We do not want to overwrite existing header here, if it was already set
|
|
if (sentryTrace && !requestHeaders.includes(`${SENTRY_TRACE_HEADER}:`)) {
|
|
request.headers += `${SENTRY_TRACE_HEADER}: ${sentryTrace}\r\n`;
|
|
}
|
|
|
|
const existingBaggage = request.headers.match(BAGGAGE_HEADER_REGEX)?.[1];
|
|
if (baggage$1 && !existingBaggage) {
|
|
request.headers += `${SENTRY_BAGGAGE_HEADER}: ${baggage$1}\r\n`;
|
|
} else if (baggage$1) {
|
|
const merged = baggage.mergeBaggageHeaders(existingBaggage, baggage$1);
|
|
if (merged) {
|
|
request.headers = request.headers.replace(BAGGAGE_HEADER_REGEX, `baggage: ${merged}\r\n`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method is called when a response is received.
|
|
*/
|
|
_onResponseHeaders({ request, response }) {
|
|
const config = this.getConfig();
|
|
const enabled = config.enabled !== false;
|
|
|
|
if (!enabled) {
|
|
return;
|
|
}
|
|
|
|
const _breadcrumbs = config.breadcrumbs;
|
|
const breadCrumbsEnabled = typeof _breadcrumbs === 'undefined' ? true : _breadcrumbs;
|
|
|
|
const shouldIgnore = this._ignoreOutgoingRequestsMap.get(request);
|
|
|
|
if (breadCrumbsEnabled && !shouldIgnore) {
|
|
addRequestBreadcrumb(request, response);
|
|
}
|
|
}
|
|
|
|
/** Subscribe to a diagnostics channel. */
|
|
_subscribeToChannel(
|
|
diagnosticChannel,
|
|
onMessage,
|
|
) {
|
|
// `diagnostics_channel` had a ref counting bug until v18.19.0.
|
|
// https://github.com/nodejs/node/pull/47520
|
|
const useNewSubscribe = nodeVersion.NODE_MAJOR > 18 || (nodeVersion.NODE_MAJOR === 18 && nodeVersion.NODE_MINOR >= 19);
|
|
|
|
let unsubscribe;
|
|
if (useNewSubscribe) {
|
|
diagch.subscribe?.(diagnosticChannel, onMessage);
|
|
unsubscribe = () => diagch.unsubscribe?.(diagnosticChannel, onMessage);
|
|
} else {
|
|
const channel = diagch.channel(diagnosticChannel);
|
|
channel.subscribe(onMessage);
|
|
unsubscribe = () => channel.unsubscribe(onMessage);
|
|
}
|
|
|
|
this._channelSubs.push({
|
|
name: diagnosticChannel,
|
|
unsubscribe,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if the given outgoing request should be ignored.
|
|
*/
|
|
_shouldIgnoreOutgoingRequest(request) {
|
|
if (core$1.isTracingSuppressed(api.context.active())) {
|
|
return true;
|
|
}
|
|
|
|
// Add trace propagation headers
|
|
const url = getAbsoluteUrl(request.origin, request.path);
|
|
const ignoreOutgoingRequests = this.getConfig().ignoreOutgoingRequests;
|
|
|
|
if (typeof ignoreOutgoingRequests !== 'function' || !url) {
|
|
return false;
|
|
}
|
|
|
|
return ignoreOutgoingRequests(url);
|
|
}
|
|
}
|
|
|
|
/** Add a breadcrumb for outgoing requests. */
|
|
function addRequestBreadcrumb(request, response) {
|
|
const data = getBreadcrumbData(request);
|
|
|
|
const statusCode = response.statusCode;
|
|
const level = core.getBreadcrumbLogLevelFromHttpStatusCode(statusCode);
|
|
|
|
core.addBreadcrumb(
|
|
{
|
|
category: 'http',
|
|
data: {
|
|
status_code: statusCode,
|
|
...data,
|
|
},
|
|
type: 'http',
|
|
level,
|
|
},
|
|
{
|
|
event: 'response',
|
|
request,
|
|
response,
|
|
},
|
|
);
|
|
}
|
|
|
|
function getBreadcrumbData(request) {
|
|
try {
|
|
const url = getAbsoluteUrl(request.origin, request.path);
|
|
const parsedUrl = core.parseUrl(url);
|
|
|
|
const data = {
|
|
url: core.getSanitizedUrlString(parsedUrl),
|
|
'http.method': request.method || 'GET',
|
|
};
|
|
|
|
if (parsedUrl.search) {
|
|
data['http.query'] = parsedUrl.search;
|
|
}
|
|
if (parsedUrl.hash) {
|
|
data['http.fragment'] = parsedUrl.hash;
|
|
}
|
|
|
|
return data;
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function getAbsoluteUrl(origin, path = '/') {
|
|
try {
|
|
const url = new URL(path, origin);
|
|
return url.toString();
|
|
} catch {
|
|
// fallback: Construct it on our own
|
|
const url = `${origin}`;
|
|
|
|
if (url.endsWith('/') && path.startsWith('/')) {
|
|
return `${url}${path.slice(1)}`;
|
|
}
|
|
|
|
if (!url.endsWith('/') && !path.startsWith('/')) {
|
|
return `${url}/${path.slice(1)}`;
|
|
}
|
|
|
|
return `${url}${path}`;
|
|
}
|
|
}
|
|
|
|
exports.SentryNodeFetchInstrumentation = SentryNodeFetchInstrumentation;
|
|
//# sourceMappingURL=SentryNodeFetchInstrumentation.js.map
|