Skip to content
Open
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: 5 additions & 5 deletions packages/core/src/tracing/ai/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,15 @@ export interface InstrumentedMethodEntry {
export type InstrumentedMethodRegistry = Record<string, InstrumentedMethodEntry>;

/**
* Resolves AI recording options by falling back to the client's `sendDefaultPii` setting.
* Precedence: explicit option > sendDefaultPii > false
* Resolves AI recording options by falling back to the client's `dataCollection.genAI` settings.
* Precedence: explicit option > dataCollection.genAI > sendDefaultPii > false
*/
export function resolveAIRecordingOptions<T extends AIRecordingOptions>(options?: T): T & Required<AIRecordingOptions> {
const sendDefaultPii = Boolean(getClient()?.getOptions().sendDefaultPii);
const genAI = getClient()?.getDataCollectionOptions().genAI;
return {
...options,
recordInputs: options?.recordInputs ?? sendDefaultPii,
recordOutputs: options?.recordOutputs ?? sendDefaultPii,
recordInputs: options?.recordInputs ?? genAI?.inputs ?? false,
recordOutputs: options?.recordOutputs ?? genAI?.outputs ?? false,
} as T & Required<AIRecordingOptions>;
}

Expand Down
58 changes: 51 additions & 7 deletions packages/core/test/lib/tracing/ai/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,71 @@ describe('resolveAIRecordingOptions', () => {
getGlobalScope().clear();
});

function setup(sendDefaultPii: boolean): void {
function setupWithSendDefaultPii(sendDefaultPii: boolean): void {
const options = getDefaultTestClientOptions({ tracesSampleRate: 1, sendDefaultPii });
const client = new TestClient(options);
setCurrentClient(client);
client.init();
}

it('defaults to false when sendDefaultPii is false', () => {
setup(false);
function setupWithDataCollection(genAI: { inputs?: boolean; outputs?: boolean }): void {
const options = getDefaultTestClientOptions({ tracesSampleRate: 1, dataCollection: { genAI } });
const client = new TestClient(options);
setCurrentClient(client);
client.init();
}

it('defaults to false when no client is set', () => {
expect(resolveAIRecordingOptions()).toEqual({ recordInputs: false, recordOutputs: false });
});

it('defaults to false when sendDefaultPii is false (bridge)', () => {
setupWithSendDefaultPii(false);
expect(resolveAIRecordingOptions()).toEqual({ recordInputs: false, recordOutputs: false });
});

it('respects sendDefaultPii: true', () => {
setup(true);
it('defaults to true when sendDefaultPii is true (bridge)', () => {
setupWithSendDefaultPii(true);
expect(resolveAIRecordingOptions()).toEqual({ recordInputs: true, recordOutputs: true });
});

it('explicit options override sendDefaultPii bridge', () => {
setupWithSendDefaultPii(true);
expect(resolveAIRecordingOptions({ recordInputs: false })).toEqual({ recordInputs: false, recordOutputs: true });
});

it('respects dataCollection.genAI.inputs and outputs', () => {
setupWithDataCollection({ inputs: true, outputs: true });
expect(resolveAIRecordingOptions()).toEqual({ recordInputs: true, recordOutputs: true });
});

it('explicit options override sendDefaultPii', () => {
setup(true);
it('respects dataCollection.genAI.inputs: false, outputs: false', () => {
setupWithDataCollection({ inputs: false, outputs: false });
expect(resolveAIRecordingOptions()).toEqual({ recordInputs: false, recordOutputs: false });
});

it('supports asymmetric dataCollection.genAI (inputs: true, outputs: false)', () => {
setupWithDataCollection({ inputs: true, outputs: false });
expect(resolveAIRecordingOptions()).toEqual({ recordInputs: true, recordOutputs: false });
});

it('supports asymmetric dataCollection.genAI (inputs: false, outputs: true)', () => {
setupWithDataCollection({ inputs: false, outputs: true });
expect(resolveAIRecordingOptions()).toEqual({ recordInputs: false, recordOutputs: true });
});

it('explicit options override dataCollection.genAI', () => {
setupWithDataCollection({ inputs: true, outputs: true });
expect(resolveAIRecordingOptions({ recordInputs: false })).toEqual({ recordInputs: false, recordOutputs: true });
});

it('explicit false overrides dataCollection.genAI.inputs: true', () => {
setupWithDataCollection({ inputs: true, outputs: true });
expect(resolveAIRecordingOptions({ recordInputs: false, recordOutputs: false })).toEqual({
recordInputs: false,
recordOutputs: false,
});
});
});

describe('wrapPromiseWithMethods', () => {
Expand Down
14 changes: 7 additions & 7 deletions packages/node/src/integrations/tracing/anthropic-ai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,18 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => {
*
* ## Options
*
* - `recordInputs`: Whether to record prompt messages (default: respects `sendDefaultPii` client option)
* - `recordOutputs`: Whether to record response text (default: respects `sendDefaultPii` client option)
* - `recordInputs`: Whether to record prompt messages (default: follows `sendDefaultPii` or `dataCollection.genAI.inputs`)
* - `recordOutputs`: Whether to record response text (default: follows `sendDefaultPii` or `dataCollection.genAI.outputs`)
*
* ### Default Behavior
*
* By default, the integration will:
* - Record inputs and outputs ONLY if `sendDefaultPii` is set to `true` in your Sentry client options
* - Otherwise, inputs and outputs are NOT recorded unless explicitly enabled
* - Record inputs and outputs based on `sendDefaultPii` or `dataCollection.genAI` in your Sentry client options
* - Integration-level `recordInputs`/`recordOutputs` options take precedence over global config
*
* @example
* ```javascript
* // Record inputs and outputs when sendDefaultPii is false
* // Always record inputs and outputs regardless of global dataCollection config
* Sentry.init({
* integrations: [
* Sentry.anthropicAIIntegration({
Expand All @@ -58,9 +58,9 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => {
* ],
* });
*
* // Never record inputs/outputs regardless of sendDefaultPii
* // Never record inputs/outputs regardless of global dataCollection config
* Sentry.init({
* sendDefaultPii: true,
* dataCollection: { genAI: { inputs: true, outputs: true } },
* integrations: [
* Sentry.anthropicAIIntegration({
* recordInputs: false,
Expand Down
14 changes: 7 additions & 7 deletions packages/node/src/integrations/tracing/langgraph/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ const _langGraphIntegration = ((options: LangGraphOptions = {}) => {
*
* ## Options
*
* - `recordInputs`: Whether to record input messages (default: respects `sendDefaultPii` client option)
* - `recordOutputs`: Whether to record response text (default: respects `sendDefaultPii` client option)
* - `recordInputs`: Whether to record prompt messages (default: follows `sendDefaultPii` or `dataCollection.genAI.inputs`)
* - `recordOutputs`: Whether to record response text (default: follows `sendDefaultPii` or `dataCollection.genAI.outputs`)
*
* ### Default Behavior
*
* By default, the integration will:
* - Record inputs and outputs ONLY if `sendDefaultPii` is set to `true` in your Sentry client options
* - Otherwise, inputs and outputs are NOT recorded unless explicitly enabled
* - Record inputs and outputs based on `sendDefaultPii` or `dataCollection.genAI` in your Sentry client options
* - Integration-level `recordInputs`/`recordOutputs` options take precedence over global config
*
* @example
* ```javascript
* // Record inputs and outputs when sendDefaultPii is false
* // Always record inputs and outputs regardless of global dataCollection config
* Sentry.init({
* integrations: [
* Sentry.langGraphIntegration({
Expand All @@ -57,9 +57,9 @@ const _langGraphIntegration = ((options: LangGraphOptions = {}) => {
* ],
* });
*
* // Never record inputs/outputs regardless of sendDefaultPii
* // Never record inputs/outputs regardless of global dataCollection config
* Sentry.init({
* sendDefaultPii: true,
* dataCollection: { genAI: { inputs: true, outputs: true } },
* integrations: [
* Sentry.langGraphIntegration({
* recordInputs: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ export class SentryLangGraphInstrumentation extends InstrumentationBase<LangGrap
*/
private _patch(exports: PatchedModuleExports): PatchedModuleExports | void {
const client = getClient();
const genAI = client?.getDataCollectionOptions().genAI;
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 code client?.getDataCollectionOptions().genAI may throw a TypeError. If client is undefined, accessing .genAI on the resulting undefined will cause a crash.
Severity: HIGH

Suggested Fix

Add optional chaining to the .genAI property access to prevent the TypeError. The corrected code should be client?.getDataCollectionOptions()?.genAI.

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/node/src/integrations/tracing/langgraph/instrumentation.ts#L94

Potential issue: The code at
`packages/node/src/integrations/tracing/langgraph/instrumentation.ts:94` uses optional
chaining for `client` but not for the subsequent property access. If `getClient()`
returns `undefined`, which can happen if a module using `@langchain/langgraph` is loaded
before `Sentry.init()` is called, the expression `client?.getDataCollectionOptions()`
evaluates to `undefined`. The following access to `.genAI` on `undefined` will then
throw a `TypeError: Cannot read properties of undefined (reading 'genAI')`, causing a
crash.

Also affects:

  • packages/core/src/tracing/ai/utils.ts:52~52

Did we get this right? 👍 / 👎 to inform future reviews.

const options = {
...this.getConfig(),
recordInputs: this.getConfig().recordInputs ?? client?.getOptions().sendDefaultPii,
recordOutputs: this.getConfig().recordOutputs ?? client?.getOptions().sendDefaultPii,
recordInputs: this.getConfig().recordInputs ?? genAI?.inputs ?? false,
recordOutputs: this.getConfig().recordOutputs ?? genAI?.outputs ?? false,
};

// Patch StateGraph.compile to instrument both compile() and invoke()
Expand Down
14 changes: 7 additions & 7 deletions packages/node/src/integrations/tracing/openai/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,18 @@ const _openAiIntegration = ((options: OpenAiOptions = {}) => {
*
* ## Options
*
* - `recordInputs`: Whether to record prompt messages (default: respects `sendDefaultPii` client option)
* - `recordOutputs`: Whether to record response text (default: respects `sendDefaultPii` client option)
* - `recordInputs`: Whether to record prompt messages (default: follows `sendDefaultPii` or `dataCollection.genAI.inputs`)
* - `recordOutputs`: Whether to record response text (default: follows `sendDefaultPii` or `dataCollection.genAI.outputs`)
*
* ### Default Behavior
*
* By default, the integration will:
* - Record inputs and outputs ONLY if `sendDefaultPii` is set to `true` in your Sentry client options
* - Otherwise, inputs and outputs are NOT recorded unless explicitly enabled
* - Record inputs and outputs based on `sendDefaultPii` or `dataCollection.genAI` in your Sentry client options
* - Integration-level `recordInputs`/`recordOutputs` options take precedence over global config
*
* @example
* ```javascript
* // Record inputs and outputs when sendDefaultPii is false
* // Always record inputs and outputs regardless of global dataCollection config
* Sentry.init({
* integrations: [
* Sentry.openAIIntegration({
Expand All @@ -57,9 +57,9 @@ const _openAiIntegration = ((options: OpenAiOptions = {}) => {
* ],
* });
*
* // Never record inputs/outputs regardless of sendDefaultPii
* // Never record inputs/outputs regardless of global dataCollection config
* Sentry.init({
* sendDefaultPii: true,
* dataCollection: { genAI: { inputs: true, outputs: true } },
* integrations: [
* Sentry.openAIIntegration({
* recordInputs: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,14 @@ export function cleanupToolCallSpanContexts(content: Array<object>): void {
* 1. The vercel ai integration options
* 2. The experimental_telemetry options in the vercel ai method calls
* 3. When telemetry is explicitly enabled (isEnabled: true), default to recording
* 4. Otherwise, use the sendDefaultPii option from client options
* 4. Otherwise, use the dataCollection.genAI settings from client options
*/
export function determineRecordingSettings(
integrationRecordingOptions: RecordingOptions | undefined,
methodTelemetryOptions: RecordingOptions,
telemetryExplicitlyEnabled: boolean | undefined,
defaultRecordingEnabled: boolean,
defaultInputsEnabled: boolean,
defaultOutputsEnabled: boolean,
): { recordInputs: boolean; recordOutputs: boolean } {
const recordInputs =
integrationRecordingOptions?.recordInputs !== undefined
Expand All @@ -173,16 +174,16 @@ export function determineRecordingSettings(
? methodTelemetryOptions.recordInputs
: telemetryExplicitlyEnabled === true
? true // When telemetry is explicitly enabled, default to recording inputs
: defaultRecordingEnabled;
: defaultInputsEnabled;

const recordOutputs =
integrationRecordingOptions?.recordOutputs !== undefined
? integrationRecordingOptions.recordOutputs
: methodTelemetryOptions.recordOutputs !== undefined
? methodTelemetryOptions.recordOutputs
: telemetryExplicitlyEnabled === true
? true // When telemetry is explicitly enabled, default to recording inputs
: defaultRecordingEnabled;
? true // When telemetry is explicitly enabled, default to recording outputs
: defaultOutputsEnabled;

return { recordInputs, recordOutputs };
}
Expand Down Expand Up @@ -239,13 +240,14 @@ export class SentryVercelAiInstrumentation extends InstrumentationBase {
const client = getClient();
const integration = client?.getIntegrationByName<VercelAiIntegration>(INTEGRATION_NAME);
const integrationOptions = integration?.options;
const shouldRecordInputsAndOutputs = integration ? Boolean(client?.getOptions().sendDefaultPii) : false;
const genAI = integration ? client?.getDataCollectionOptions().genAI : undefined;

const { recordInputs, recordOutputs } = determineRecordingSettings(
integrationOptions,
existingExperimentalTelemetry,
isEnabled,
shouldRecordInputsAndOutputs,
Boolean(genAI?.inputs),
Boolean(genAI?.outputs),
);

args[0].experimental_telemetry = {
Expand Down
10 changes: 6 additions & 4 deletions packages/node/src/integrations/tracing/vercelai/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ export declare type AttributeValue =

export interface VercelAiOptions {
/**
* Enable or disable input recording. Enabled if `sendDefaultPii` is `true`
* or if you set `isEnabled` to `true` in your ai SDK method telemetry settings
* Enable or disable input recording. Enabled if `sendDefaultPii` or `dataCollection.genAI.inputs` is `true`
* or if you set `isEnabled` to `true` in your ai SDK method telemetry settings.
* Integration-level options take precedence over global `dataCollection` config.
*/
recordInputs?: boolean;
/**
* Enable or disable output recording. Enabled if `sendDefaultPii` is `true`
* or if you set `isEnabled` to `true` in your ai SDK method telemetry settings
* Enable or disable output recording. Enabled if `sendDefaultPii` or `dataCollection.genAI.outputs` is `true`
* or if you set `isEnabled` to `true` in your ai SDK method telemetry settings.
* Integration-level options take precedence over global `dataCollection` config.
*/
recordOutputs?: boolean;

Expand Down
Loading
Loading