diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 3deeb1ae0df4..d124ed198a2b 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -23,6 +23,7 @@ "test:watch": "yarn test --watch" }, "dependencies": { + "@anthropic-ai/sdk": "0.63.0", "@aws-sdk/client-s3": "^3.552.0", "@hapi/hapi": "^21.3.10", "@nestjs/common": "11.1.3", diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs index 9344137a4ed3..bbbefef79148 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-options.mjs @@ -1,5 +1,4 @@ import * as Sentry from '@sentry/node'; -import { nodeContextIntegration } from '@sentry/node-core'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ @@ -13,6 +12,12 @@ Sentry.init({ recordInputs: true, recordOutputs: true, }), - nodeContextIntegration(), ], + beforeSendTransaction: event => { + // Filter out mock express server transactions + if (event.transaction.includes('/anthropic/v1/')) { + return null; + } + return event; + }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs index c2776c15b001..8c6bbcc3ce0a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument-with-pii.mjs @@ -1,5 +1,4 @@ import * as Sentry from '@sentry/node'; -import { nodeContextIntegration } from '@sentry/node-core'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ @@ -8,5 +7,11 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: true, transport: loggingTransport, - integrations: [Sentry.anthropicAIIntegration(), nodeContextIntegration()], + beforeSendTransaction: event => { + // Filter out mock express server transactions + if (event.transaction.includes('/anthropic/v1/')) { + return null; + } + return event; + }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs index 39f1506eb2c9..2b8a197791e2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/instrument.mjs @@ -1,5 +1,4 @@ import * as Sentry from '@sentry/node'; -import { nodeContextIntegration } from '@sentry/node-core'; import { loggingTransport } from '@sentry-internal/node-integration-tests'; Sentry.init({ @@ -8,6 +7,11 @@ Sentry.init({ tracesSampleRate: 1.0, sendDefaultPii: false, transport: loggingTransport, - // Force include the integration - integrations: [Sentry.anthropicAIIntegration(), nodeContextIntegration()], + beforeSendTransaction: event => { + // Filter out mock express server transactions + if (event.transaction.includes('/anthropic/v1/')) { + return null; + } + return event; + }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-manual-client.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-manual-client.mjs new file mode 100644 index 000000000000..590796931315 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario-manual-client.mjs @@ -0,0 +1,115 @@ +import { instrumentAnthropicAiClient } from '@sentry/core'; +import * as Sentry from '@sentry/node'; + +class MockAnthropic { + constructor(config) { + this.apiKey = config.apiKey; + + // Create messages object with create and countTokens methods + this.messages = { + create: this._messagesCreate.bind(this), + countTokens: this._messagesCountTokens.bind(this), + }; + + this.models = { + retrieve: this._modelsRetrieve.bind(this), + }; + } + + /** + * Create a mock message + */ + async _messagesCreate(params) { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + if (params.model === 'error-model') { + const error = new Error('Model not found'); + error.status = 404; + error.headers = { 'x-request-id': 'mock-request-123' }; + throw error; + } + + return { + id: 'msg_mock123', + type: 'message', + model: params.model, + role: 'assistant', + content: [ + { + type: 'text', + text: 'Hello from Anthropic mock!', + }, + ], + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: 10, + output_tokens: 15, + }, + }; + } + + async _messagesCountTokens() { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + // For countTokens, just return input_tokens + return { + input_tokens: 15, + }; + } + + async _modelsRetrieve(modelId) { + // Simulate processing time + await new Promise(resolve => setTimeout(resolve, 10)); + + // Match what the actual implementation would return + return { + id: modelId, + name: modelId, + created_at: 1715145600, + model: modelId, // Add model field to match the check in addResponseAttributes + }; + } +} + +async function run() { + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { + const mockClient = new MockAnthropic({ + apiKey: 'mock-api-key', + }); + + const client = instrumentAnthropicAiClient(mockClient); + + // First test: basic message completion + await client.messages.create({ + model: 'claude-3-haiku-20240307', + system: 'You are a helpful assistant.', + messages: [{ role: 'user', content: 'What is the capital of France?' }], + temperature: 0.7, + max_tokens: 100, + }); + + // Second test: error handling + try { + await client.messages.create({ + model: 'error-model', + messages: [{ role: 'user', content: 'This will fail' }], + }); + } catch { + // Error is expected and handled + } + + // Third test: count tokens with cached tokens + await client.messages.countTokens({ + model: 'claude-3-haiku-20240307', + messages: [{ role: 'user', content: 'What is the capital of France?' }], + }); + + // Fourth test: models.retrieve + await client.models.retrieve('claude-3-haiku-20240307'); + }); +} + +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario.mjs b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario.mjs index 590796931315..d0acf5c42b79 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario.mjs +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/scenario.mjs @@ -1,39 +1,40 @@ -import { instrumentAnthropicAiClient } from '@sentry/core'; +import Anthropic from '@anthropic-ai/sdk'; import * as Sentry from '@sentry/node'; +import express from 'express'; -class MockAnthropic { - constructor(config) { - this.apiKey = config.apiKey; +const PORT = 3333; - // Create messages object with create and countTokens methods - this.messages = { - create: this._messagesCreate.bind(this), - countTokens: this._messagesCountTokens.bind(this), - }; +function startMockAnthropicServer() { + const app = express(); + app.use(express.json()); - this.models = { - retrieve: this._modelsRetrieve.bind(this), - }; - } + app.post('/anthropic/v1/messages/count_tokens', (req, res) => { + res.send({ + input_tokens: 15, + }); + }); - /** - * Create a mock message - */ - async _messagesCreate(params) { - // Simulate processing time - await new Promise(resolve => setTimeout(resolve, 10)); + app.get('/anthropic/v1/models/:model', (req, res) => { + res.send({ + id: req.params.model, + name: req.params.model, + created_at: 1715145600, + model: req.params.model, + }); + }); + + app.post('/anthropic/v1/messages', (req, res) => { + const model = req.body.model; - if (params.model === 'error-model') { - const error = new Error('Model not found'); - error.status = 404; - error.headers = { 'x-request-id': 'mock-request-123' }; - throw error; + if (model === 'error-model') { + res.status(404).set('x-request-id', 'mock-request-123').send('Model not found'); + return; } - return { + res.send({ id: 'msg_mock123', type: 'message', - model: params.model, + model, role: 'assistant', content: [ { @@ -47,41 +48,20 @@ class MockAnthropic { input_tokens: 10, output_tokens: 15, }, - }; - } - - async _messagesCountTokens() { - // Simulate processing time - await new Promise(resolve => setTimeout(resolve, 10)); - - // For countTokens, just return input_tokens - return { - input_tokens: 15, - }; - } - - async _modelsRetrieve(modelId) { - // Simulate processing time - await new Promise(resolve => setTimeout(resolve, 10)); - - // Match what the actual implementation would return - return { - id: modelId, - name: modelId, - created_at: 1715145600, - model: modelId, // Add model field to match the check in addResponseAttributes - }; - } + }); + }); + return app.listen(PORT); } async function run() { + const server = startMockAnthropicServer(); + await Sentry.startSpan({ op: 'function', name: 'main' }, async () => { - const mockClient = new MockAnthropic({ + const client = new Anthropic({ apiKey: 'mock-api-key', + baseURL: `http://localhost:${PORT}/anthropic`, }); - const client = instrumentAnthropicAiClient(mockClient); - // First test: basic message completion await client.messages.create({ model: 'claude-3-haiku-20240307', @@ -110,6 +90,8 @@ async function run() { // Fourth test: models.retrieve await client.models.retrieve('claude-3-haiku-20240307'); }); + + server.close(); } run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts index 27a0a523b927..9c14f698bc18 100644 --- a/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/anthropic/test.ts @@ -189,6 +189,16 @@ describe('Anthropic integration', () => { ]), }; + createEsmAndCjsTests(__dirname, 'scenario-manual-client.mjs', 'instrument.mjs', (createRunner, test) => { + test('creates anthropic related spans when manually insturmenting client', async () => { + await createRunner() + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION_DEFAULT_PII_FALSE }) + .start() + .completed(); + }); + }); + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { test('creates anthropic related spans with sendDefaultPii: false', async () => { await createRunner() diff --git a/packages/core/src/utils/anthropic-ai/index.ts b/packages/core/src/utils/anthropic-ai/index.ts index cf99b12c1062..f24707c4cc92 100644 --- a/packages/core/src/utils/anthropic-ai/index.ts +++ b/packages/core/src/utils/anthropic-ai/index.ts @@ -1,4 +1,4 @@ -import { getCurrentScope } from '../../currentScopes'; +import { getClient } from '../../currentScopes'; import { captureException } from '../../exports'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../semanticAttributes'; import { SPAN_STATUS_ERROR } from '../../tracing'; @@ -25,11 +25,9 @@ import { } from '../ai/gen-ai-attributes'; import { buildMethodPath, getFinalOperationName, getSpanOperation, setTokenUsageAttributes } from '../ai/utils'; import { handleCallbackErrors } from '../handleCallbackErrors'; -import { ANTHROPIC_AI_INTEGRATION_NAME } from './constants'; import { instrumentStream } from './streaming'; import type { AnthropicAiInstrumentedMethod, - AnthropicAiIntegration, AnthropicAiOptions, AnthropicAiResponse, AnthropicAiStreamingEvent, @@ -196,21 +194,6 @@ function addResponseAttributes(span: Span, response: AnthropicAiResponse, record addMetadataAttributes(span, response); } -/** - * Get record options from the integration - */ -function getRecordingOptionsFromIntegration(): AnthropicAiOptions { - const scope = getCurrentScope(); - const client = scope.getClient(); - const integration = client?.getIntegrationByName(ANTHROPIC_AI_INTEGRATION_NAME) as AnthropicAiIntegration | undefined; - const shouldRecordInputsAndOutputs = integration ? Boolean(client?.getOptions().sendDefaultPii) : false; - - return { - recordInputs: integration?.options?.recordInputs ?? shouldRecordInputsAndOutputs, - recordOutputs: integration?.options?.recordOutputs ?? shouldRecordInputsAndOutputs, - }; -} - /** * Instrument a method with Sentry spans * Following Sentry AI Agents Manual Instrumentation conventions @@ -220,10 +203,9 @@ function instrumentMethod( originalMethod: (...args: T) => Promise, methodPath: AnthropicAiInstrumentedMethod, context: unknown, - options?: AnthropicAiOptions, + options: AnthropicAiOptions, ): (...args: T) => Promise { return async function instrumentedMethod(...args: T): Promise { - const finalOptions = options || getRecordingOptionsFromIntegration(); const requestAttributes = extractRequestAttributes(args, methodPath); const model = requestAttributes[GEN_AI_REQUEST_MODEL_ATTRIBUTE] ?? 'unknown'; const operationName = getFinalOperationName(methodPath); @@ -241,7 +223,7 @@ function instrumentMethod( }, async span => { try { - if (finalOptions.recordInputs && params) { + if (options.recordInputs && params) { addPrivateRequestAttributes(span, params); } @@ -249,7 +231,7 @@ function instrumentMethod( return instrumentStream( result as AsyncIterable, span, - finalOptions.recordOutputs ?? false, + options.recordOutputs ?? false, ) as unknown as R; } catch (error) { span.setStatus({ code: SPAN_STATUS_ERROR, message: 'internal_error' }); @@ -276,7 +258,7 @@ function instrumentMethod( attributes: requestAttributes as Record, }, span => { - if (finalOptions.recordInputs && params) { + if (options.recordInputs && params) { addPrivateRequestAttributes(span, params); } @@ -294,7 +276,7 @@ function instrumentMethod( }); }, () => {}, - result => addResponseAttributes(span, result as AnthropicAiResponse, finalOptions.recordOutputs), + result => addResponseAttributes(span, result as AnthropicAiResponse, options.recordOutputs), ); }, ); @@ -304,7 +286,7 @@ function instrumentMethod( /** * Create a deep proxy for Anthropic AI client instrumentation */ -function createDeepProxy(target: T, currentPath = '', options?: AnthropicAiOptions): T { +function createDeepProxy(target: T, currentPath = '', options: AnthropicAiOptions): T { return new Proxy(target, { get(obj: object, prop: string): unknown { const value = (obj as Record)[prop]; @@ -337,6 +319,13 @@ function createDeepProxy(target: T, currentPath = '', options? * @param options - Optional configuration for recording inputs and outputs * @returns The instrumented client with the same type as the input */ -export function instrumentAnthropicAiClient(client: T, options?: AnthropicAiOptions): T { - return createDeepProxy(client, '', options); +export function instrumentAnthropicAiClient(anthropicAiClient: T, options?: AnthropicAiOptions): T { + const sendDefaultPii = Boolean(getClient()?.getOptions().sendDefaultPii); + + const _options = { + recordInputs: sendDefaultPii, + recordOutputs: sendDefaultPii, + ...options, + }; + return createDeepProxy(anthropicAiClient, '', _options); } diff --git a/packages/node/src/integrations/tracing/anthropic-ai/index.ts b/packages/node/src/integrations/tracing/anthropic-ai/index.ts index b9ec00013f49..65b7d72a869a 100644 --- a/packages/node/src/integrations/tracing/anthropic-ai/index.ts +++ b/packages/node/src/integrations/tracing/anthropic-ai/index.ts @@ -3,9 +3,9 @@ import { ANTHROPIC_AI_INTEGRATION_NAME, defineIntegration } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node-core'; import { SentryAnthropicAiInstrumentation } from './instrumentation'; -export const instrumentAnthropicAi = generateInstrumentOnce( +export const instrumentAnthropicAi = generateInstrumentOnce( ANTHROPIC_AI_INTEGRATION_NAME, - () => new SentryAnthropicAiInstrumentation({}), + options => new SentryAnthropicAiInstrumentation(options), ); const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { @@ -13,7 +13,7 @@ const _anthropicAIIntegration = ((options: AnthropicAiOptions = {}) => { name: ANTHROPIC_AI_INTEGRATION_NAME, options, setupOnce() { - instrumentAnthropicAi(); + instrumentAnthropicAi(options); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts b/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts index a10a01b3debf..d55689415aee 100644 --- a/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts +++ b/packages/node/src/integrations/tracing/anthropic-ai/instrumentation.ts @@ -4,14 +4,12 @@ import { InstrumentationBase, InstrumentationNodeModuleDefinition, } from '@opentelemetry/instrumentation'; -import type { AnthropicAiClient, AnthropicAiOptions, Integration } from '@sentry/core'; -import { ANTHROPIC_AI_INTEGRATION_NAME, getClient, instrumentAnthropicAiClient, SDK_VERSION } from '@sentry/core'; +import type { AnthropicAiClient, AnthropicAiOptions } from '@sentry/core'; +import { getClient, instrumentAnthropicAiClient, SDK_VERSION } from '@sentry/core'; const supportedVersions = ['>=0.19.2 <1.0.0']; -export interface AnthropicAiIntegration extends Integration { - options: AnthropicAiOptions; -} +type AnthropicAiInstrumentationOptions = InstrumentationConfig & AnthropicAiOptions; /** * Represents the patched shape of the Anthropic AI module export. @@ -21,23 +19,11 @@ interface PatchedModuleExports { Anthropic: abstract new (...args: unknown[]) => AnthropicAiClient; } -/** - * Determines telemetry recording settings. - */ -function determineRecordingSettings( - integrationOptions: AnthropicAiOptions | undefined, - defaultEnabled: boolean, -): { recordInputs: boolean; recordOutputs: boolean } { - const recordInputs = integrationOptions?.recordInputs ?? defaultEnabled; - const recordOutputs = integrationOptions?.recordOutputs ?? defaultEnabled; - return { recordInputs, recordOutputs }; -} - /** * Sentry Anthropic AI instrumentation using OpenTelemetry. */ -export class SentryAnthropicAiInstrumentation extends InstrumentationBase { - public constructor(config: InstrumentationConfig = {}) { +export class SentryAnthropicAiInstrumentation extends InstrumentationBase { + public constructor(config: AnthropicAiInstrumentationOptions = {}) { super('@sentry/instrumentation-anthropic-ai', SDK_VERSION, config); } @@ -59,14 +45,15 @@ export class SentryAnthropicAiInstrumentation extends InstrumentationBase(ANTHROPIC_AI_INTEGRATION_NAME); - const integrationOpts = integration?.options; const defaultPii = Boolean(client?.getOptions().sendDefaultPii); - const { recordInputs, recordOutputs } = determineRecordingSettings(integrationOpts, defaultPii); + const recordInputs = config.recordInputs ?? defaultPii; + const recordOutputs = config.recordOutputs ?? defaultPii; return instrumentAnthropicAiClient(instance as AnthropicAiClient, { recordInputs, diff --git a/yarn.lock b/yarn.lock index ebdb2b198675..82438f8947dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -328,6 +328,13 @@ resolved "https://registry.yarnpkg.com/@antfu/utils/-/utils-0.7.10.tgz#ae829f170158e297a9b6a28f161a8e487d00814d" integrity sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww== +"@anthropic-ai/sdk@0.63.0": + version "0.63.0" + resolved "https://registry.yarnpkg.com/@anthropic-ai/sdk/-/sdk-0.63.0.tgz#725ea136ebf2b0fc7ebfdcb655a1d69b60bd3927" + integrity sha512-g2KzDcVXxT2d/SMuVJHeJ6T2loj6jFMt+Nj+I6bfwXWNDMoOP0HhiWr+5RivRV7Yv++jBurDGr76XBCc66R79A== + dependencies: + json-schema-to-ts "^3.1.1" + "@apollo/protobufjs@1.2.6": version "1.2.6" resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.2.6.tgz#d601e65211e06ae1432bf5993a1a0105f2862f27" @@ -2595,10 +2602,10 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.8.4": - version "7.27.6" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.6.tgz#ec4070a04d76bae8ddbb10770ba55714a417b7c6" - integrity sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q== +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.8.4": + version "7.28.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326" + integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ== "@babel/standalone@^7.23.8": version "7.24.7" @@ -20104,6 +20111,14 @@ json-parse-even-better-errors@^3.0.0: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz#2cb2ee33069a78870a0c7e3da560026b89669cf7" integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA== +json-schema-to-ts@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz#81f3acaf5a34736492f6f5f51870ef9ece1ca853" + integrity sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g== + dependencies: + "@babel/runtime" "^7.18.3" + ts-algebra "^2.0.0" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -28770,7 +28785,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" @@ -29538,6 +29552,11 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +ts-algebra@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ts-algebra/-/ts-algebra-2.0.0.tgz#4e3e0953878f26518fce7f6bb115064a65388b7a" + integrity sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw== + ts-api-utils@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.3.tgz#f12c1c781d04427313dbac808f453f050e54a331"