Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/astro/src/index.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { handleRequest } from './server/middleware';
// Hence, we export everything from the Node SDK explicitly:
export {
addBreadcrumb,
addAttachment,
addEventProcessor,
addIntegration,
amqplibIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/aws-serverless/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
addEventProcessor,
addBreadcrumb,
addAttachment,
addIntegration,
captureException,
captureEvent,
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type { BrowserOptions } from './client';
export {
addEventProcessor,
addBreadcrumb,
addAttachment,
addIntegration,
captureException,
captureEvent,
Expand Down
1 change: 1 addition & 0 deletions packages/bun/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type {
export {
addEventProcessor,
addBreadcrumb,
addAttachment,
addIntegration,
captureException,
captureEvent,
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type { CloudflareOptions } from './client';

export {
addEventProcessor,
addAttachment,
addBreadcrumb,
addIntegration,
captureException,
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/attachments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getClient, getIsolationScope } from './currentScopes';
import type { Attachment } from './types-hoist/attachment';
/**
* Records a new breadcrumb which will be attached to future events.
*
* Breadcrumbs will be added to subsequent events to provide more context on
* user's actions prior to an error or crash.
*/
export function addAttachment(attachment: Attachment): void {
const client = getClient();
const isolationScope = getIsolationScope();

if (!client) return;

// const mergedAttachment = { timestamp, ...attachment };
// const finalAttachment = beforeAttachment
// ? consoleSandbox(() => beforeAttachment(mergedAttachment, hint))
// : mergedAttachment;

// if (finalAttachment === null) return;

// if (client.emit) {
// client.emit('beforeAddAttachment', finalAttachment, hint);
// }

isolationScope.addAttachment(attachment);
}
45 changes: 45 additions & 0 deletions packages/core/src/envelope.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Client } from './client';
import { getDynamicSamplingContextFromSpan } from './tracing/dynamicSamplingContext';
import type { SentrySpan } from './tracing/sentrySpan';
import type { Attachment } from './types-hoist/attachment';
import type { LegacyCSPReport } from './types-hoist/csp';
import type { DsnComponents } from './types-hoist/dsn';
import type {
AttachmentEnvelope,
DynamicSamplingContext,
EventEnvelope,
EventItem,
Expand All @@ -23,11 +25,13 @@ import {
createEnvelope,
createEventEnvelopeHeaders,
createSpanEnvelopeItem,
createTraceAttachmentEnvelopeItem,
getSdkMetadataForEnvelopeHeader,
} from './utils/envelope';
import { uuid4 } from './utils/misc';
import { shouldIgnoreSpan } from './utils/should-ignore-span';
import { showSpanDropWarning, spanToJSON } from './utils/spanUtils';
import { timestampInSeconds } from './utils/time';

/**
* Apply SdkInfo (name, version, packages, integrations) to the corresponding event key.
Expand Down Expand Up @@ -196,3 +200,44 @@ export function createRawSecurityEnvelope(

return createEnvelope<RawSecurityEnvelope>(envelopeHeaders, [eventItem]);
}

/**
* Create envelope from Attachment item using the trace attachment format.
*
* This creates a standalone attachment envelope with trace context according to
* the experimental trace attachment specification:
* https://develop.sentry.dev/sdk/data-model/envelope-items/#trace-attachment
*
* The attachment payload includes metadata with trace_id, attachment_id,
* timestamp, and optional attributes.
*
* @param attachment - The attachment to send
* @param dsc - Dynamic Sampling Context containing trace information
* @param dsn - DSN components for the envelope header
* @param tunnel - Tunnel URL if configured
* @param attributes - Optional arbitrary attributes for querying in EAP
* @param traceId - The trace_id to associate with this attachment
*/
export function createAttachmentEnvelope(
attachment: Attachment,
dsc: Partial<DynamicSamplingContext> | undefined,
dsn: DsnComponents | undefined,
tunnel: string | undefined,
attributes: Record<string, { type: string; value: unknown }> | undefined,
traceId: string,
): AttachmentEnvelope {
function dscHasRequiredProps(dsc: Partial<DynamicSamplingContext>): dsc is DynamicSamplingContext {
return !!dsc.trace_id && !!dsc.public_key;
}

const headers: AttachmentEnvelope[0] = {
sent_at: new Date().toISOString(),
...(dsc && dscHasRequiredProps(dsc) && { trace: dsc }),
...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
};

const timestamp = timestampInSeconds();
const attachmentItem = createTraceAttachmentEnvelopeItem(attachment, traceId, timestamp, attributes);

return createEnvelope<AttachmentEnvelope>(headers, [attachmentItem]);
}
5 changes: 4 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type { IntegrationIndex } from './integration';

export * from './tracing';
export * from './semanticAttributes';
export { createEventEnvelope, createSessionEnvelope, createSpanEnvelope } from './envelope';
export { createAttachmentEnvelope, createEventEnvelope, createSessionEnvelope, createSpanEnvelope } from './envelope';
export {
captureCheckIn,
withMonitor,
Expand Down Expand Up @@ -103,6 +103,7 @@ export {
} from './utils/request';
export { DEFAULT_ENVIRONMENT, DEV_ENVIRONMENT } from './constants';
export { addBreadcrumb } from './breadcrumbs';
export { addAttachment } from './attachments';
export { functionToStringIntegration } from './integrations/functiontostring';
// eslint-disable-next-line deprecation/deprecation
export { inboundFiltersIntegration } from './integrations/eventFilters';
Expand Down Expand Up @@ -280,6 +281,7 @@ export {
createEnvelope,
createEventEnvelopeHeaders,
createSpanEnvelopeItem,
createTraceAttachmentEnvelopeItem,
envelopeContainsItemType,
envelopeItemTypeToDataCategory,
forEachEnvelopeItem,
Expand Down Expand Up @@ -349,6 +351,7 @@ export type { DataCategory } from './types-hoist/datacategory';
export type { DsnComponents, DsnLike, DsnProtocol } from './types-hoist/dsn';
export type { DebugImage, DebugMeta } from './types-hoist/debugMeta';
export type {
AttachmentEnvelope,
AttachmentItem,
BaseEnvelopeHeaders,
BaseEnvelopeItemHeaders,
Expand Down
19 changes: 18 additions & 1 deletion packages/core/src/scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
import type { AttributeObject, RawAttribute, RawAttributes } from './attributes';
import type { Client } from './client';
import { DEBUG_BUILD } from './debug-build';
import { createAttachmentEnvelope } from './envelope';
import { updateSession } from './session';
import { getDynamicSamplingContextFromSpan } from './tracing/dynamicSamplingContext';
import type { Attachment } from './types-hoist/attachment';
import type { Breadcrumb } from './types-hoist/breadcrumb';
import type { Context, Contexts } from './types-hoist/context';
Expand All @@ -23,6 +25,7 @@ import { merge } from './utils/merge';
import { uuid4 } from './utils/misc';
import { generateTraceId } from './utils/propagationContext';
import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope';
import { getActiveSpan } from './utils/spanUtils';
import { truncate } from './utils/string';
import { dateTimestampInSeconds } from './utils/time';

Expand Down Expand Up @@ -604,9 +607,23 @@ export class Scope {

/**
* Add an attachment to the scope.
*
* For the trace attachments PoC, this will immediately send the attachment
* in a standalone envelope with trace context.
*/
public addAttachment(attachment: Attachment): this {
this._attachments.push(attachment);
const span = getActiveSpan();
const dsc = span ? getDynamicSamplingContextFromSpan(span) : undefined;
const traceId = span?.spanContext().traceId || dsc?.trace_id || this._propagationContext.traceId;
const dsn = this._client?.getDsn();
const tunnel = this._client?.getOptions().tunnel;
const envelope = createAttachmentEnvelope(attachment, dsc, dsn, tunnel, undefined, traceId);

void this._client?.sendEnvelope(envelope);

// Still add to the scope's attachments array for backward compatibility
// this._attachments.push(attachment);

return this;
}

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/types-hoist/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,10 @@ type EventItemHeaders = {
type AttachmentItemHeaders = {
type: 'attachment';
length: number;
filename: string;
filename?: string;
content_type?: string;
attachment_type?: AttachmentType;
meta_length?: number;
};
type UserFeedbackItemHeaders = { type: 'user_report' };
type FeedbackItemHeaders = { type: 'feedback' };
Expand Down Expand Up @@ -133,6 +134,7 @@ type CheckInEnvelopeHeaders = { trace?: DynamicSamplingContext };
type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders;
type ReplayEnvelopeHeaders = BaseEnvelopeHeaders;
type SpanEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext };
type AttachmentEnvelopeHeaders = BaseEnvelopeHeaders & { trace?: DynamicSamplingContext };
type LogEnvelopeHeaders = BaseEnvelopeHeaders;
type MetricEnvelopeHeaders = BaseEnvelopeHeaders;
export type EventEnvelope = BaseEnvelope<
Expand All @@ -144,6 +146,7 @@ export type ClientReportEnvelope = BaseEnvelope<ClientReportEnvelopeHeaders, Cli
export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]];
export type CheckInEnvelope = BaseEnvelope<CheckInEnvelopeHeaders, CheckInItem>;
export type SpanEnvelope = BaseEnvelope<SpanEnvelopeHeaders, SpanItem>;
export type AttachmentEnvelope = BaseEnvelope<AttachmentEnvelopeHeaders, AttachmentItem>;
export type ProfileChunkEnvelope = BaseEnvelope<BaseEnvelopeHeaders, ProfileChunkItem>;
export type RawSecurityEnvelope = BaseEnvelope<BaseEnvelopeHeaders, RawSecurityItem>;
export type LogEnvelope = BaseEnvelope<LogEnvelopeHeaders, LogContainerItem>;
Expand All @@ -157,6 +160,7 @@ export type Envelope =
| ReplayEnvelope
| CheckInEnvelope
| SpanEnvelope
| AttachmentEnvelope
| RawSecurityEnvelope
| LogEnvelope
| MetricEnvelope;
Expand Down
62 changes: 61 additions & 1 deletion packages/core/src/utils/envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { SdkInfo } from '../types-hoist/sdkinfo';
import type { SdkMetadata } from '../types-hoist/sdkmetadata';
import type { SpanJSON } from '../types-hoist/span';
import { dsnToString } from './dsn';
import { uuid4 } from './misc';
import { normalize } from './normalize';
import { GLOBAL_OBJ } from './worldwide';

Expand Down Expand Up @@ -187,7 +188,7 @@ export function createSpanEnvelopeItem(spanJson: Partial<SpanJSON>): SpanItem {
}

/**
* Creates attachment envelope items
* Creates attachment envelope items (traditional format - for events)
*/
export function createAttachmentEnvelopeItem(attachment: Attachment): AttachmentItem {
const buffer = typeof attachment.data === 'string' ? encodeUTF8(attachment.data) : attachment.data;
Expand All @@ -204,6 +205,65 @@ export function createAttachmentEnvelopeItem(attachment: Attachment): Attachment
];
}

/**
* Creates a trace attachment envelope item with metadata prefix.
*
* This implements the experimental trace attachment format as specified at:
* https://develop.sentry.dev/sdk/data-model/envelope-items/#trace-attachment
*
* @param attachment - The attachment to create an item for
* @param traceId - The trace ID (32 character hex string)
* @param timestamp - UNIX timestamp as a float
* @param attributes - Optional arbitrary attributes for querying
*/
export function createTraceAttachmentEnvelopeItem(
attachment: Attachment,
traceId: string,
timestamp: number,
attributes?: Record<string, { type: string; value: unknown }>,
): AttachmentItem {
const attachmentBody = typeof attachment.data === 'string' ? encodeUTF8(attachment.data) : attachment.data;

// Generate a unique attachment ID (UUID v4 without dashes)
const attachmentId = uuid4().replace(/-/g, '');

// Create the metadata JSON object
const metadata: Record<string, unknown> = {
trace_id: traceId,
attachment_id: attachmentId,
timestamp,
content_type: attachment.contentType || 'application/octet-stream',
};

if (attachment.filename) {
metadata.filename = attachment.filename;
}

if (attributes) {
metadata.attributes = attributes;
}

// Serialize metadata to JSON
const metadataJson = JSON.stringify(metadata);
const metadataBuffer = encodeUTF8(metadataJson);

// Combine metadata and attachment body
const combinedLength = metadataBuffer.length + attachmentBody.length;
const combinedBuffer = new Uint8Array(combinedLength);
combinedBuffer.set(metadataBuffer, 0);
combinedBuffer.set(attachmentBody, metadataBuffer.length);

return [
{
type: 'attachment',
content_type: 'application/vnd.sentry.trace-attachment',
length: combinedLength,
meta_length: metadataBuffer.length,
},
combinedBuffer,
];
}

const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record<EnvelopeItemType, DataCategory> = {
session: 'session',
sessions: 'session',
Expand Down
1 change: 1 addition & 0 deletions packages/deno/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export type {
export type { DenoOptions } from './types';

export {
addAttachment,
addEventProcessor,
addBreadcrumb,
captureException,
Expand Down
1 change: 1 addition & 0 deletions packages/google-cloud-serverless/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export {
addAttachment,
addEventProcessor,
addBreadcrumb,
addIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/node-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {
} from '@sentry/opentelemetry';

export {
addAttachment,
addBreadcrumb,
isInitialized,
isEnabled,
Expand Down
1 change: 1 addition & 0 deletions packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export {

export {
addBreadcrumb,
addAttachment,
isInitialized,
isEnabled,
getGlobalScope,
Expand Down
1 change: 1 addition & 0 deletions packages/remix/src/cloudflare/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export type {
} from '@sentry/core';

export {
addAttachment,
addEventProcessor,
addBreadcrumb,
addIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/remix/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// We need to explicitly export @sentry/node as they end up under `default` in ESM builds
// See: https://github.com/getsentry/sentry-javascript/issues/8474
export {
addAttachment,
addBreadcrumb,
addEventProcessor,
addIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/solidstart/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
// on the top - level namespace.
// Hence, we export everything from the Node SDK explicitly:
export {
addAttachment,
addBreadcrumb,
addEventProcessor,
addIntegration,
Expand Down
1 change: 1 addition & 0 deletions packages/sveltekit/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// on the top - level namespace.
// Hence, we export everything from the Node SDK explicitly:
export {
addAttachment,
addBreadcrumb,
addEventProcessor,
addIntegration,
Expand Down
Loading
Loading