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
10 changes: 9 additions & 1 deletion .oxlintrc.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,15 @@
{
"files": ["**/integrations/tracing-channel/aws-sdk/**/*.ts"],
"rules": {
"typescript/no-explicit-any": "off"
"typescript/no-explicit-any": "off",
"typescript/no-unsafe-member-access": "off"
}
},
{
"files": ["**/integrations/tracing-channel/aws-sdk/services/**/*.ts"],
"rules": {
"max-lines": "off",
"complexity": "off"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@

/** The span origin every aws-sdk channel span carries, mirroring the uniform OTel `auto.otel.aws`. */
export const AWS_SDK_ORIGIN = 'auto.aws.orchestrion.aws_sdk';

/** DynamoDB `db.system` value (an attribute value, not a key, so not covered by conventions). */
export const DB_SYSTEM_VALUE_DYNAMODB = 'dynamodb';
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import type { Span } from '@sentry/core';
import type { NormalizedRequest, NormalizedResponse, RequestMetadata } from '../types';
import { DynamodbServiceExtension } from './dynamodb';
import { KinesisServiceExtension } from './kinesis';
import { S3ServiceExtension } from './s3';
import { SecretsManagerServiceExtension } from './secretsmanager';
import type { ServiceExtension } from './ServiceExtension';
import { StepFunctionsServiceExtension } from './stepfunctions';

export class ServicesExtensions implements ServiceExtension {
// Per-service extensions, keyed by the client's `serviceId` (e.g. `'S3'`). Services without a
// registered extension still get the base rpc span from the subscriber.
private _services: Map<string, ServiceExtension> = new Map();
private _services: Map<string, ServiceExtension> = new Map<string, ServiceExtension>([
['SecretsManager', new SecretsManagerServiceExtension()],
['SFN', new StepFunctionsServiceExtension()],
['DynamoDB', new DynamodbServiceExtension()],
['S3', new S3ServiceExtension()],
['Kinesis', new KinesisServiceExtension()],
]);

public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
const serviceExtension = this._services.get(request.serviceName);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { Span } from '@sentry/core';
import { SPAN_KIND } from '@sentry/core';
import {
AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS as ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS,
AWS_DYNAMODB_CONSISTENT_READ as ATTR_AWS_DYNAMODB_CONSISTENT_READ,
AWS_DYNAMODB_CONSUMED_CAPACITY as ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY,
AWS_DYNAMODB_COUNT as ATTR_AWS_DYNAMODB_COUNT,
AWS_DYNAMODB_EXCLUSIVE_START_TABLE as ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE,
AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES as ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES,
AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES as ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES,
AWS_DYNAMODB_INDEX_NAME as ATTR_AWS_DYNAMODB_INDEX_NAME,
AWS_DYNAMODB_ITEM_COLLECTION_METRICS as ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS,
AWS_DYNAMODB_LIMIT as ATTR_AWS_DYNAMODB_LIMIT,
AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES as ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES,
AWS_DYNAMODB_PROJECTION as ATTR_AWS_DYNAMODB_PROJECTION,
AWS_DYNAMODB_PROVISIONED_READ_CAPACITY as ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY,
AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY as ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY,
AWS_DYNAMODB_SCAN_FORWARD as ATTR_AWS_DYNAMODB_SCAN_FORWARD,
AWS_DYNAMODB_SCANNED_COUNT as ATTR_AWS_DYNAMODB_SCANNED_COUNT,
AWS_DYNAMODB_SEGMENT as ATTR_AWS_DYNAMODB_SEGMENT,
AWS_DYNAMODB_SELECT as ATTR_AWS_DYNAMODB_SELECT,
AWS_DYNAMODB_TABLE_COUNT as ATTR_AWS_DYNAMODB_TABLE_COUNT,
AWS_DYNAMODB_TABLE_NAMES as ATTR_AWS_DYNAMODB_TABLE_NAMES,
AWS_DYNAMODB_TOTAL_SEGMENTS as ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS,
DB_NAME,
DB_OPERATION,
DB_SYSTEM,
} from '@sentry/conventions/attributes';
import { DB_SYSTEM_VALUE_DYNAMODB } from '../constants';
import type { NormalizedRequest, NormalizedResponse } from '../types';
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';

function toArray<T>(values: T | T[]): T[] {
return Array.isArray(values) ? values : [values];
}

export class DynamodbServiceExtension implements ServiceExtension {
public requestPreSpanHook(normalizedRequest: NormalizedRequest): RequestMetadata {
const operation = normalizedRequest.commandName;
const tableName = normalizedRequest.commandInput?.TableName;

const spanAttributes: Record<string, unknown> = {};

/* oxlint-disable typescript/no-deprecated -- old-semconv db.* attributes, matched to the OTel aws-sdk integration */
spanAttributes[DB_SYSTEM] = DB_SYSTEM_VALUE_DYNAMODB;
spanAttributes[DB_NAME] = tableName;
spanAttributes[DB_OPERATION] = operation;
/* oxlint-enable typescript/no-deprecated */

// `RequestItems` is undefined when no table names are returned; its keys are the table names.
if (normalizedRequest.commandInput?.TableName) {
// Necessary for commands with only 1 table name (e.g. CreateTable). Attribute is `TableName`, not keys of `RequestItems`.
spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = [normalizedRequest.commandInput.TableName];
} else if (normalizedRequest.commandInput?.RequestItems) {
spanAttributes[ATTR_AWS_DYNAMODB_TABLE_NAMES] = Object.keys(normalizedRequest.commandInput.RequestItems);
}

if (operation === 'CreateTable' || operation === 'UpdateTable') {
// only check for ProvisionedThroughput since ReadCapacityUnits and WriteCapacityUnits are required attributes
if (normalizedRequest.commandInput?.ProvisionedThroughput) {
spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_READ_CAPACITY] =
normalizedRequest.commandInput.ProvisionedThroughput.ReadCapacityUnits;
spanAttributes[ATTR_AWS_DYNAMODB_PROVISIONED_WRITE_CAPACITY] =
normalizedRequest.commandInput.ProvisionedThroughput.WriteCapacityUnits;
}
}

if (operation === 'GetItem' || operation === 'Scan' || operation === 'Query') {
if (normalizedRequest.commandInput?.ConsistentRead) {
spanAttributes[ATTR_AWS_DYNAMODB_CONSISTENT_READ] = normalizedRequest.commandInput.ConsistentRead;
}
}

if (operation === 'Query' || operation === 'Scan') {
if (normalizedRequest.commandInput?.ProjectionExpression) {
spanAttributes[ATTR_AWS_DYNAMODB_PROJECTION] = normalizedRequest.commandInput.ProjectionExpression;
}
}

if (operation === 'CreateTable') {
if (normalizedRequest.commandInput?.GlobalSecondaryIndexes) {
spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEXES] = toArray(
normalizedRequest.commandInput.GlobalSecondaryIndexes,
).map((x: unknown) => JSON.stringify(x));
}

if (normalizedRequest.commandInput?.LocalSecondaryIndexes) {
spanAttributes[ATTR_AWS_DYNAMODB_LOCAL_SECONDARY_INDEXES] = toArray(
normalizedRequest.commandInput.LocalSecondaryIndexes,
).map((x: unknown) => JSON.stringify(x));
}
}

if (operation === 'ListTables' || operation === 'Query' || operation === 'Scan') {
if (normalizedRequest.commandInput?.Limit) {
spanAttributes[ATTR_AWS_DYNAMODB_LIMIT] = normalizedRequest.commandInput.Limit;
}
}

if (operation === 'ListTables') {
if (normalizedRequest.commandInput?.ExclusiveStartTableName) {
spanAttributes[ATTR_AWS_DYNAMODB_EXCLUSIVE_START_TABLE] =
normalizedRequest.commandInput.ExclusiveStartTableName;
}
}

if (operation === 'Query') {
if (normalizedRequest.commandInput?.ScanIndexForward) {
spanAttributes[ATTR_AWS_DYNAMODB_SCAN_FORWARD] = normalizedRequest.commandInput.ScanIndexForward;
}
Comment on lines +108 to +110

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The check for ScanIndexForward uses a truthy conditional, which incorrectly omits the span attribute when the value is explicitly false, a meaningful state for DynamoDB queries.
Severity: MEDIUM

Suggested Fix

Modify the conditional to explicitly handle both true and false values. Instead of a simple truthy check, verify the property is not undefined, for example: if (normalizedRequest.commandInput?.ScanIndexForward !== undefined). This will ensure the attribute is set correctly for both true and false values.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts#L108-L110

Potential issue: The code checks for the `ScanIndexForward` parameter using `if
(normalizedRequest.commandInput?.ScanIndexForward)`. This conditional only evaluates to
true for truthy values, causing it to fail when `ScanIndexForward` is explicitly set to
`false`. Since `false` is a valid and meaningful value indicating a reverse-order scan,
the `aws.dynamodb.scan_forward` span attribute is incorrectly omitted in this scenario.
This leads to a loss of observability for DynamoDB queries that request results in
descending order. A similar issue exists for the `ConsistentRead` parameter.


if (normalizedRequest.commandInput?.IndexName) {
spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName;
}

if (normalizedRequest.commandInput?.Select) {
spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select;
}
}

if (operation === 'Scan') {
if (normalizedRequest.commandInput?.Segment) {
spanAttributes[ATTR_AWS_DYNAMODB_SEGMENT] = normalizedRequest.commandInput?.Segment;
}
Comment thread
andreiborza marked this conversation as resolved.
Comment on lines +122 to +124

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The truthy check on commandInput.Segment fails when its value is 0, causing the span attribute for the first segment of a DynamoDB parallel scan to be dropped.
Severity: LOW

Suggested Fix

Modify the conditional check to correctly handle the case where Segment is 0. Instead of a truthy check, explicitly test if the value is not undefined or is a number, for example: if (normalizedRequest.commandInput?.Segment !== undefined).

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
packages/server-utils/src/integrations/tracing-channel/aws-sdk/services/dynamodb.ts#L122-L124

Potential issue: In DynamoDB parallel scans, the `Segment` attribute is a zero-based
integer. The code checks for its existence with a simple truthy check: `if
(normalizedRequest.commandInput?.Segment)`. Because the number `0` is falsy in
JavaScript, this condition evaluates to false when `Segment` is `0`. As a result, the
`ATTR_AWS_DYNAMODB_SEGMENT` attribute is never added to the span for the first segment
of any parallel scan, leading to incomplete telemetry data for this operation.


if (normalizedRequest.commandInput?.TotalSegments) {
spanAttributes[ATTR_AWS_DYNAMODB_TOTAL_SEGMENTS] = normalizedRequest.commandInput?.TotalSegments;
}

if (normalizedRequest.commandInput?.IndexName) {
spanAttributes[ATTR_AWS_DYNAMODB_INDEX_NAME] = normalizedRequest.commandInput.IndexName;
}

if (normalizedRequest.commandInput?.Select) {
spanAttributes[ATTR_AWS_DYNAMODB_SELECT] = normalizedRequest.commandInput.Select;
}
}

if (operation === 'UpdateTable') {
if (normalizedRequest.commandInput?.AttributeDefinitions) {
spanAttributes[ATTR_AWS_DYNAMODB_ATTRIBUTE_DEFINITIONS] = toArray(
normalizedRequest.commandInput.AttributeDefinitions,
).map((x: unknown) => JSON.stringify(x));
}

if (normalizedRequest.commandInput?.GlobalSecondaryIndexUpdates) {
spanAttributes[ATTR_AWS_DYNAMODB_GLOBAL_SECONDARY_INDEX_UPDATES] = toArray(
normalizedRequest.commandInput.GlobalSecondaryIndexUpdates,
).map((x: unknown) => JSON.stringify(x));
}
}

return {
spanAttributes,
spanKind: SPAN_KIND.CLIENT,
// Matches what the exporter infers from `db.system` for the OTel DynamoDB spans.
spanOp: 'db',
};
}

public responseHook(response: NormalizedResponse, span: Span): void {
if (response.data?.ConsumedCapacity) {
span.setAttribute(
ATTR_AWS_DYNAMODB_CONSUMED_CAPACITY,
toArray(response.data.ConsumedCapacity).map((x: unknown) => JSON.stringify(x)),
);
}

if (response.data?.ItemCollectionMetrics) {
span.setAttribute(
ATTR_AWS_DYNAMODB_ITEM_COLLECTION_METRICS,
toArray(response.data.ItemCollectionMetrics).map((x: unknown) => JSON.stringify(x)),
);
}
Comment thread
sentry[bot] marked this conversation as resolved.

if (response.data?.TableNames) {
span.setAttribute(ATTR_AWS_DYNAMODB_TABLE_COUNT, response.data?.TableNames.length);
}

if (response.data?.Count) {
span.setAttribute(ATTR_AWS_DYNAMODB_COUNT, response.data?.Count);
}

if (response.data?.ScannedCount) {
span.setAttribute(ATTR_AWS_DYNAMODB_SCANNED_COUNT, response.data?.ScannedCount);
Comment thread
andreiborza marked this conversation as resolved.
}
Comment thread
andreiborza marked this conversation as resolved.
}
}
Comment thread
andreiborza marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { SPAN_KIND } from '@sentry/core';
import { _AWS_KINESIS_STREAM_NAME as AWS_KINESIS_STREAM_NAME } from '@sentry/conventions/attributes';
import type { NormalizedRequest } from '../types';
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';

export class KinesisServiceExtension implements ServiceExtension {
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
const streamName = request.commandInput?.StreamName;
const spanAttributes: Record<string, unknown> = {};

if (streamName) {
// oxlint-disable-next-line typescript/no-deprecated -- old-semconv aws.kinesis.stream.name, matched to the OTel aws-sdk integration
spanAttributes[AWS_KINESIS_STREAM_NAME] = streamName;
}

return {
spanAttributes,
spanKind: SPAN_KIND.CLIENT,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { SPAN_KIND } from '@sentry/core';
import { AWS_S3_BUCKET } from '@sentry/conventions/attributes';
import type { NormalizedRequest } from '../types';
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';

export class S3ServiceExtension implements ServiceExtension {
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
const bucketName = request.commandInput?.Bucket;
const spanAttributes: Record<string, unknown> = {};

if (bucketName) {
spanAttributes[AWS_S3_BUCKET] = bucketName;
}

return {
spanAttributes,
spanKind: SPAN_KIND.CLIENT,
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Span } from '@sentry/core';
import { SPAN_KIND } from '@sentry/core';
import { AWS_SECRETSMANAGER_SECRET_ARN as ATTR_AWS_SECRETSMANAGER_SECRET_ARN } from '@sentry/conventions/attributes';
import type { NormalizedRequest, NormalizedResponse } from '../types';
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';

export class SecretsManagerServiceExtension implements ServiceExtension {
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
const secretId = request.commandInput?.SecretId;
const spanAttributes: Record<string, unknown> = {};
if (typeof secretId === 'string' && secretId.startsWith('arn:aws:secretsmanager:')) {
spanAttributes[ATTR_AWS_SECRETSMANAGER_SECRET_ARN] = secretId;
}

return {
spanAttributes,
spanKind: SPAN_KIND.CLIENT,
};
}

public responseHook(response: NormalizedResponse, span: Span): void {
const secretArn = response.data?.ARN;
if (secretArn) {
span.setAttribute(ATTR_AWS_SECRETSMANAGER_SECRET_ARN, secretArn);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { SPAN_KIND } from '@sentry/core';
import {
AWS_STEP_FUNCTIONS_ACTIVITY_ARN as ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN,
AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN as ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN,
} from '@sentry/conventions/attributes';
import type { NormalizedRequest } from '../types';
import type { RequestMetadata, ServiceExtension } from './ServiceExtension';

export class StepFunctionsServiceExtension implements ServiceExtension {
public requestPreSpanHook(request: NormalizedRequest): RequestMetadata {
const stateMachineArn = request.commandInput?.stateMachineArn;
const activityArn = request.commandInput?.activityArn;
const spanAttributes: Record<string, unknown> = {};

if (stateMachineArn) {
spanAttributes[ATTR_AWS_STEP_FUNCTIONS_STATE_MACHINE_ARN] = stateMachineArn;
}

if (activityArn) {
spanAttributes[ATTR_AWS_STEP_FUNCTIONS_ACTIVITY_ARN] = activityArn;
}

return {
spanAttributes,
spanKind: SPAN_KIND.CLIENT,
};
}
}
Loading