Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,22 +15,19 @@ interface AwsLambdaOptions {
disableAwsContextPropagation?: boolean;
}

export const instrumentAwsLambda = generateInstrumentOnce<AwsLambdaOptions>(
export const instrumentAwsLambda = generateInstrumentOnce(
'AwsLambda',
(_options: AwsLambdaOptions = {}) => {
const options = {
AwsLambdaInstrumentation,
(options: AwsLambdaOptions) => {
return {
disableAwsContextPropagation: true,
..._options,
};

return new AwsLambdaInstrumentation({
...options,
eventContextExtractor,
requestHook(span) {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.aws.lambda');
},
});
};
},
);

Expand Down
21 changes: 12 additions & 9 deletions packages/node/src/integrations/node-fetch/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration, getClient } from '
import { generateInstrumentOnce } from '../../otel/instrument';
import type { NodeClient } from '../../sdk/client';
import type { NodeClientOptions } from '../../types';
import type { SentryNodeFetchInstrumentationOptions } from './SentryNodeFetchInstrumentation';
import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation';

const INTEGRATION_NAME = 'NodeFetch';
Expand DownExpand Up@@ -33,14 +32,19 @@ interface NodeFetchOptions {
ignoreOutgoingRequests?: (url: string) => boolean;
}

const instrumentOtelNodeFetch = generateInstrumentOnce<UndiciInstrumentationConfig>(INTEGRATION_NAME, config => {
return new UndiciInstrumentation(config);
});
const instrumentOtelNodeFetch = generateInstrumentOnce(
INTEGRATION_NAME,
UndiciInstrumentation,
(options: NodeFetchOptions) => {
return getConfigWithDefaults(options);
},
);

const instrumentSentryNodeFetch = generateInstrumentOnce<SentryNodeFetchInstrumentationOptions>(
const instrumentSentryNodeFetch = generateInstrumentOnce(
`${INTEGRATION_NAME}.sentry`,
config => {
return new SentryNodeFetchInstrumentation(config);
SentryNodeFetchInstrumentation,
(options: NodeFetchOptions) => {
return options;
},
);

Expand All@@ -52,8 +56,7 @@ const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => {

// This is the "regular" OTEL instrumentation that emits spans
if (instrumentSpans) {
const instrumentationConfig = getConfigWithDefaults(options);
instrumentOtelNodeFetch(instrumentationConfig);
instrumentOtelNodeFetch(options);
}

// This is the Sentry-specific instrumentation that creates breadcrumbs & propagates traces
Expand Down
9 changes: 5 additions & 4 deletions packages/node/src/integrations/tracing/graphql.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,12 +37,13 @@ interface GraphqlOptions {

const INTEGRATION_NAME = 'Graphql';

export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
export const instrumentGraphql = generateInstrumentOnce(
INTEGRATION_NAME,
(_options: GraphqlOptions = {}) => {
GraphQLInstrumentation,
(_options: GraphqlOptions) => {
const options = getOptionsWithDefaults(_options);

return new GraphQLInstrumentation({
return {
...options,
responseHook(span) {
addOriginToSpan(span, 'auto.graphql.otel.graphql');
Expand DownExpand Up@@ -73,7 +74,7 @@ export const instrumentGraphql = generateInstrumentOnce<GraphqlOptions>(
}
}
},
});
};
},
);

Expand Down
73 changes: 69 additions & 4 deletions packages/node/src/otel/instrument.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,16 +3,47 @@ import { type Instrumentation, registerInstrumentations } from '@opentelemetry/i
/** Exported only for tests. */
export const INSTRUMENTED: Record<string, Instrumentation> = {};

/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string };
export function generateInstrumentOnce<
Options = unknown,
InstrumentationInstance extends Instrumentation = Instrumentation,
>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string };
/**
* Instrument an OpenTelemetry instrumentation once.
* This will skip running instrumentation again if it was already instrumented.
*/
export function generateInstrumentOnce<Options>(
name: string,
creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation),
optionsCallback?: (options: Options) => unknown,
): ((options: Options) => Instrumentation) & { id: string } {
if (optionsCallback) {
return _generateInstrumentOnceWithOptions(
name,
creatorOrClass as new (...args: unknown[]) => Instrumentation,
optionsCallback,
);
}

return _generateInstrumentOnce(name, creatorOrClass as (options?: Options) => Instrumentation);
}

// The plain version without handling of options
// Should not be used with custom options that are mutated in the creator!
function _generateInstrumentOnce<Options = unknown, InstrumentationInstance extends Instrumentation = Instrumentation>(
name: string,
creator: (options?: Options) => InstrumentationInstance,
): ((options?: Options) => InstrumentationInstance) & { id: string } {
return Object.assign(
(options?: Options) => {
Expand All@@ -38,6 +69,40 @@ export function generateInstrumentOnce<
);
}

// This version handles options properly
function _generateInstrumentOnceWithOptions<
Options,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstrumentationClass extends new (...args: any[]) => Instrumentation,
>(
name: string,
instrumentationClass: InstrumentationClass,
optionsCallback: (options: Options) => ConstructorParameters<InstrumentationClass>[0],
): ((options: Options) => InstanceType<InstrumentationClass>) & { id: string } {
return Object.assign(
(_options: Options) => {
const options = optionsCallback(_options);

const instrumented = INSTRUMENTED[name] as InstanceType<InstrumentationClass> | undefined;
if (instrumented) {
// Ensure we update options
instrumented.setConfig(options);
return instrumented;
}

const instrumentation = new instrumentationClass(options) as InstanceType<InstrumentationClass>;
INSTRUMENTED[name] = instrumentation;

registerInstrumentations({
instrumentations: [instrumentation],
});

return instrumentation;
},
{ id: name },
);
}

/**
* Ensure a given callback is called when the instrumentation is actually wrapping something.
* This can be used to ensure some logic is only called when the instrumentation is actually active.
Expand Down
20 changes: 11 additions & 9 deletions packages/remix/src/server/integrations/opentelemetry.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,22 +7,24 @@ import type { RemixOptions } from '../../utils/remixOptions';

const INTEGRATION_NAME = 'Remix';

const instrumentRemix = generateInstrumentOnce<RemixOptions>(
INTEGRATION_NAME,
(_options?: RemixOptions) =>
new RemixInstrumentation({
actionFormDataAttributes: _options?.sendDefaultPii ? _options?.captureActionFormDataKeys : undefined,
}),
);
interface RemixInstrumentationOptions {
actionFormDataAttributes?: Record<string, string | boolean>;
}

const instrumentRemix = generateInstrumentOnce(INTEGRATION_NAME, (options?: RemixInstrumentationOptions) => {
return new RemixInstrumentation(options);
});

const _remixIntegration = (() => {
return {
name: 'Remix',
setupOnce() {
const client = getClient();
const options = client?.getOptions();
const options = client?.getOptions() as RemixOptions | undefined;

instrumentRemix(options);
instrumentRemix({
actionFormDataAttributes: options?.sendDefaultPii ? options?.captureActionFormDataKeys : undefined,
});
},

setup(client: Client) {
Expand Down