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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});
, '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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});
, '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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});
, '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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});
, '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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});
, '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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});
, '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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});
, '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
4 changes: 4 additions & 0 deletions packages/aws-serverless/rollup.npm.config.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,10 @@ export default [
entrypoints: ['src/index.ts', 'src/awslambda-auto.ts'],
// packages with bundles have a different build directory structure
hasBundles: true,
packageSpecificConfig: {
// Used for our custom eventContextExtractor
external: ['@opentelemetry/api'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity, why is this needed?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package build structure isn't right without this. It pulls this in under its own node_modules folder.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
}),
),
...makeOtelLoaders('./build', 'sentry-node'),
Expand Down
44 changes: 34 additions & 10 deletions packages/aws-serverless/src/integration/awslambda.ts
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,44 @@
import{AwsLambdaInstrumentation}from'@opentelemetry/instrumentation-aws-lambda';
import{SEMANTIC_ATTRIBUTE_SENTRY_OP,SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,defineIntegration}from'@sentry/core';
import{addOpenTelemetryInstrumentation}from'@sentry/node';
import{generateInstrumentOnce}from'@sentry/node';
importtype{IntegrationFn}from'@sentry/types';
import{eventContextExtractor}from'../utils';

const_awsLambdaIntegration=(()=>{
interfaceAwsLambdaOptions{
/**
* Disables the AWS context propagation and instead uses
* Sentry's context. Defaults to `true`, in order for
* Sentry trace propagation to take precedence, but can
* be disabled if you want AWS propagation to take take
* precedence.
*/
disableAwsContextPropagation?: boolean;
}

exportconstinstrumentAwsLambda=generateInstrumentOnce<AwsLambdaOptions>(
'AwsLambda',
(_options: AwsLambdaOptions={})=>{
constoptions={
disableAwsContextPropagation: true,
..._options,
};

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

const_awsLambdaIntegration=((options: AwsLambdaOptions={})=>{
return{
name: 'AwsLambda',
setupOnce(){
addOpenTelemetryInstrumentation(
newAwsLambdaInstrumentation({
requestHook(span){
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,'auto.otel.aws-lambda');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP,'function.aws.lambda');
},
}),
);
instrumentAwsLambda(options);
},
};
})satisfiesIntegrationFn;
Expand Down
14 changes: 4 additions & 10 deletions packages/aws-serverless/src/sdk.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,7 +16,7 @@ import {
withScope,
} from '@sentry/node';
import type { Integration, Options, Scope, SdkMetadata, Span } from '@sentry/types';
import { isString, logger } from '@sentry/utils';
import { logger } from '@sentry/utils';
import type { Context, Handler } from 'aws-lambda';
import { performance } from 'perf_hooks';

Expand All@@ -25,7 +25,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } fr
import { DEBUG_BUILD } from './debug-build';
import { awsIntegration } from './integration/aws';
import { awsLambdaIntegration } from './integration/awslambda';
import { markEventUnhandled } from './utils';
import { getAwsTraceData, markEventUnhandled } from './utils';

const { isPromise } = types;

Expand DownExpand Up@@ -334,15 +334,9 @@ export function wrapHandler<TEvent, TResult>(
// Otherwise, we create two root spans (one from otel, one from our wrapper).
// If Otel instrumentation didn't work or was filtered by users, we still want to trace the handler.
if (options.startTrace && !isWrappedByOtel(handler)) {
const eventWithHeaders = event as { headers?: { [key: string]: string } };
const traceData = getAwsTraceData(event as { headers?: Record<string, string> }, context);

const sentryTrace =
eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])
? eventWithHeaders.headers['sentry-trace']
: undefined;
const baggage = eventWithHeaders.headers?.baggage;

return continueTrace({ sentryTrace, baggage }, () => {
return continueTrace({ sentryTrace: traceData['sentry-trace'], baggage: traceData.baggage }, () => {
return startSpanManual(
{
name: context.functionName,
Expand Down
74 changes: 73 additions & 1 deletion packages/aws-serverless/src/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
importtype{TextMapGetter}from'@opentelemetry/api';
importtype{ContextasOtelContext}from'@opentelemetry/api';
import{contextasotelContext,propagation}from'@opentelemetry/api';
importtype{Scope}from'@sentry/types';
import{addExceptionMechanism}from'@sentry/utils';
import{addExceptionMechanism,isString}from'@sentry/utils';
importtype{Handler}from'aws-lambda';
importtype{APIGatewayProxyEventHeaders}from'aws-lambda';

typeHandlerEvent=Parameters<Handler<{headers?: Record<string,string>}>>[0];
typeHandlerContext=Parameters<Handler>[1];

typeTraceData={
'sentry-trace'?: string;
baggage?: string;
};

// vendored from
// https://github.com/open-telemetry/opentelemetry-js-contrib/blob/main/plugins/node/opentelemetry-instrumentation-aws-lambda/src/instrumentation.ts#L65-L72
constheaderGetter: TextMapGetter<APIGatewayProxyEventHeaders>={
keys(carrier): string[]{
returnObject.keys(carrier);
},
get(carrier,key: string){
returncarrier[key];
},
};

/**
* Marks an event as unhandled by adding a span processor to the passed scope.
Expand All@@ -12,3 +36,51 @@ export function markEventUnhandled(scope: Scope): Scope {

returnscope;
}

/**
* Extracts sentry trace data from the handler `context` if available and falls
* back to the `event`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can also add a short note here when this would be on context and when on event?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an explanation, but tbh this is pretty wide open. I think different AWS services make use of different event/context usage. Hope this adds a bit more info?

*
* When instrumenting the Lambda function with Sentry, the sentry trace data
* is placed on `context.clientContext.Custom`. Users are free to modify context
* tho and provide this data via `event` or `context`.
*/
exportfunctiongetAwsTraceData(event: HandlerEvent,context?: HandlerContext): TraceData{
constheaders=event.headers||{};

consttraceData: TraceData={
'sentry-trace': headers['sentry-trace'],
baggage: headers.baggage,
};

if(context&&context.clientContext&&context.clientContext.Custom){
constcustomContext: Record<string,unknown>=context.clientContext.Custom;
constsentryTrace=isString(customContext['sentry-trace']) ? customContext['sentry-trace'] : undefined;

if(sentryTrace){

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: It will probably not matter in 99% of cases, but let's just split this into two if-blocks. So basically:

constsentryTrace= ...;constbaggage= ...;if(sentryTrace){traceData['sentry-trace']=sentryTrace;}if(baggage){traceData.baggage=baggage;}

I think this is slightly more robust :)

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of that, but wouldn't we potentially end up with a mix of wrong pairs, e.g. both event and context have baggage but only event has sentry-trace. So we end up with sentry-trace from event and baggage from context.

I don't know if that's a realistic usecase tho. In core, we also throw away baggage if sentryTrace is not valid.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

right, then let's keep it this way, all good! 👍

traceData['sentry-trace']=sentryTrace;
traceData.baggage=isString(customContext.baggage) ? customContext.baggage : undefined;
}
}

returntraceData;
}

/**
* A custom event context extractor for the aws integration. It takes sentry trace data
* from the context rather than the event, with the event being a fallback.
*
* Is only used when the handler was successfully wrapped by otel and the integration option
* `disableAwsContextPropagation` is `true`.
*/
exportfunctioneventContextExtractor(event: HandlerEvent,context?: HandlerContext): OtelContext{
// The default context extractor tries to get sampled trace headers from HTTP headers
// The otel aws integration packs these onto the context, so we try to extract them from
// there instead.
consthttpHeaders={
...(event.headers||{}),
...getAwsTraceData(event,context),
};

returnpropagation.extract(otelContext.active(),httpHeaders,headerGetter);
}
102 changes: 102 additions & 0 deletions packages/aws-serverless/test/utils.test.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
import { eventContextExtractor, getAwsTraceData } from '../src/utils';

const mockExtractContext = jest.fn();
jest.mock('@opentelemetry/api', () => {
const actualApi = jest.requireActual('@opentelemetry/api');
return {
...actualApi,
propagation: {
extract: (...args: unknown[]) => mockExtractContext(args),
},
};
});

const mockContext = {
clientContext: {
Custom: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-1',
baggage: 'sentry-environment=production',
},
},
};
const mockEvent = {
headers: {
'sentry-trace': '12345678901234567890123456789012-1234567890123456-2',
baggage: 'sentry-environment=staging',
},
};

describe('getTraceData', () => {
test('gets sentry trace data from the context', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData({}, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the context even if event has data', () => {
// @ts-expect-error, a partial context object is fine here
const traceData = getAwsTraceData(mockEvent, mockContext);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-1');
expect(traceData.baggage).toEqual('sentry-environment=production');
});

test('gets sentry trace data from the event if no context is passed', () => {
const traceData = getAwsTraceData(mockEvent);

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});

test('gets sentry trace data from the event if the context sentry trace is undefined', () => {
const traceData = getAwsTraceData(mockEvent, {
// @ts-expect-error, a partial context object is fine here
clientContext: { Custom: { 'sentry-trace': undefined, baggage: '' } },
});

expect(traceData['sentry-trace']).toEqual('12345678901234567890123456789012-1234567890123456-2');
expect(traceData.baggage).toEqual('sentry-environment=staging');
});
});

describe('eventContextExtractor', () => {
afterEach(() => {
jest.clearAllMocks();
});

test('passes sentry trace data to the propagation extractor', () => {
// @ts-expect-error, a partial context object is fine here
eventContextExtractor(mockEvent, mockContext);

// @ts-expect-error, a partial context object is fine here
const expectedTraceData = getAwsTraceData(mockEvent, mockContext);

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedTraceData]));
});

test('passes along non-sentry trace headers along', () => {
eventContextExtractor(
{
...mockEvent,
headers: {
...mockEvent.headers,
'X-Custom-Header': 'Foo',
},
},
// @ts-expect-error, a partial context object is fine here
mockContext,
);

const expectedHeaders = {
'X-Custom-Header': 'Foo',
// @ts-expect-error, a partial context object is fine here
...getAwsTraceData(mockEvent, mockContext),
};

expect(mockExtractContext).toHaveBeenCalledTimes(1);
expect(mockExtractContext).toHaveBeenCalledWith(expect.arrayContaining([expectedHeaders]));
});
});