Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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: 2 additions & 2 deletions packages/server-utils/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default [
...orchestrionRuntimeHooks,
...makeNPMConfigVariants(
makeBaseNPMConfig({
// `src/orchestrion/config.ts` and `src/orchestrion/bundler/vite.ts` are
// `src/orchestrion/config/index.ts` and `src/orchestrion/bundler/vite.ts` are
// loaded via dedicated subpath exports (`.../orchestrion/config`,
// `.../orchestrion/vite`) — neither is reachable from `src/index.ts`, so we
// list them as separate entrypoints to guarantee they end up in build/esm
Expand All @@ -28,7 +28,7 @@ export default [
entrypoints: [
'src/index.ts',
'src/orchestrion/index.ts',
'src/orchestrion/config.ts',
'src/orchestrion/config/index.ts',
Comment thread
cursor[bot] marked this conversation as resolved.
// `src/orchestrion/runtime/register.ts` backs the `./orchestrion/register`
// subpath export; the Node SDK `require`s it synchronously from
// `Sentry.init()` to install the channel-injection hooks.
Expand Down
42 changes: 15 additions & 27 deletions packages/server-utils/src/orchestrion/channels.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { mysqlChannels } from './config/mysql';
import { lruMemoizerChannels } from './config/lru-memoizer';
import { ioredisChannels } from './config/ioredis';
import { pgChannels } from './config/pg';
import { openaiChannels } from './config/openai';
import { anthropicAiChannels } from './config/anthropic-ai';
import { vercelAiChannels } from './config/vercel-ai';

/**
* Fully-qualified `diagnostics_channel` names that orchestrion publishes to.
*
Expand All @@ -12,33 +20,13 @@
* they don't drift apart and silently stop firing.
*/
export const CHANNELS = {
MYSQL_QUERY: 'orchestrion:mysql:query',
LRU_MEMOIZER_LOAD: 'orchestrion:lru-memoizer:load',
IOREDIS_COMMAND: 'orchestrion:ioredis:command',
IOREDIS_CONNECT: 'orchestrion:ioredis:connect',
PG_QUERY: 'orchestrion:pg:query',
PG_CONNECT: 'orchestrion:pg:connect',
PGPOOL_CONNECT: 'orchestrion:pg-pool:connect',
OPENAI_CHAT: 'orchestrion:openai:chat',
OPENAI_RESPONSES: 'orchestrion:openai:responses',
OPENAI_EMBEDDINGS: 'orchestrion:openai:embeddings',
OPENAI_CONVERSATIONS: 'orchestrion:openai:conversations',
ANTHROPIC_CHAT: 'orchestrion:@anthropic-ai/sdk:chat',
ANTHROPIC_MODELS: 'orchestrion:@anthropic-ai/sdk:models',
ANTHROPIC_MESSAGES_STREAM: 'orchestrion:@anthropic-ai/sdk:messages-stream',
// Vercel AI (`ai`) v6: orchestrion injects these so the same channel-based
// integration that consumes `ai`'s native `ai:telemetry` channel (v7) can
// also instrument v6. Each maps to a top-level function in `ai`'s bundle.
VERCEL_AI_GENERATE_TEXT: 'orchestrion:ai:generateText',
VERCEL_AI_STREAM_TEXT: 'orchestrion:ai:streamText',
VERCEL_AI_EMBED: 'orchestrion:ai:embed',
VERCEL_AI_EMBED_MANY: 'orchestrion:ai:embedMany',
VERCEL_AI_EXECUTE_TOOL_CALL: 'orchestrion:ai:executeToolCall',
// `resolveLanguageModel` is the single chokepoint every model call flows
// through; we wrap it to monkey-patch `doGenerate`/`doStream` on the returned
// model (the model-call site itself is an inline call with no injectable
// definition).
VERCEL_AI_RESOLVE_LANGUAGE_MODEL: 'orchestrion:ai:resolveLanguageModel',
...mysqlChannels,
...lruMemoizerChannels,
...ioredisChannels,
...pgChannels,
...openaiChannels,
...anthropicAiChannels,
...vercelAiChannels,

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.

YES. 👏

} as const;

export type ChannelName = (typeof CHANNELS)[keyof typeof CHANNELS];
215 changes: 0 additions & 215 deletions packages/server-utils/src/orchestrion/config.ts

This file was deleted.

40 changes: 40 additions & 0 deletions packages/server-utils/src/orchestrion/config/anthropic-ai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';

export const anthropicAiConfig = [
// One entry each for CJS/ESM
...['resources/messages/messages.js', 'resources/messages/messages.mjs'].flatMap(filePath =>
(['create', 'countTokens'] as const).map(methodName => ({
channelName: 'chat',
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.19.2 <1', filePath },
functionQuery: { className: 'Messages', methodName, kind: 'Auto' as const },
})),
),
...['resources/completions.js', 'resources/completions.mjs'].map(filePath => ({
channelName: 'chat',
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.19.2 <1', filePath },
functionQuery: { className: 'Completions', methodName: 'create', kind: 'Auto' as const },
})),
...['resources/beta/messages/messages.js', 'resources/beta/messages/messages.mjs'].map(filePath => ({
channelName: 'chat',
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.19.2 <1', filePath },
functionQuery: { className: 'Messages', methodName: 'create', kind: 'Auto' as const },
})),
...['resources/models.js', 'resources/models.mjs'].map(filePath => ({
channelName: 'models',
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.19.2 <1', filePath },
functionQuery: { className: 'Models', methodName: 'retrieve', kind: 'Auto' as const },
})),
// `messages.stream()` returns a synchronous emitter, not a promise, so `kind: 'Sync'` is required:
// `Auto`'s promise wrapper never publishes `end` for a non-thenable return, so the span would never end.
...['resources/messages/messages.js', 'resources/messages/messages.mjs'].map(filePath => ({
channelName: 'messages-stream',
module: { name: '@anthropic-ai/sdk', versionRange: '>=0.19.2 <1', filePath },
functionQuery: { className: 'Messages', methodName: 'stream', kind: 'Sync' as const },
})),
] satisfies InstrumentationConfig[];

export const anthropicAiChannels = {
ANTHROPIC_CHAT: 'orchestrion:@anthropic-ai/sdk:chat',
ANTHROPIC_MODELS: 'orchestrion:@anthropic-ai/sdk:models',
ANTHROPIC_MESSAGES_STREAM: 'orchestrion:@anthropic-ai/sdk:messages-stream',
} as const;
48 changes: 48 additions & 0 deletions packages/server-utils/src/orchestrion/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { InstrumentationConfig } from '@apm-js-collab/code-transformer';
import { mysqlConfig } from './mysql';
import { lruMemoizerConfig } from './lru-memoizer';
import { ioredisConfig } from './ioredis';
import { openaiConfig } from './openai';
import { pgConfig } from './pg';
import { anthropicAiConfig } from './anthropic-ai';
import { vercelAiConfig } from './vercel-ai';

export const SENTRY_INSTRUMENTATIONS: InstrumentationConfig[] = [
...mysqlConfig,
...lruMemoizerConfig,
...ioredisConfig,
...openaiConfig,
...pgConfig,
...anthropicAiConfig,
...vercelAiConfig,
];

/**
* The unique set of package names instrumented by `SENTRY_INSTRUMENTATIONS`
* (e.g. `['mysql']`).
*
* Bundler plugins MUST ensure these are actually bundled rather than
* externalized: an externalized dependency is resolved from `node_modules` at
* runtime and never passes through the code transform's `onLoad`, so its
* diagnostics_channel calls are silently never injected.
*/
export const INSTRUMENTED_MODULE_NAMES: string[] = Array.from(new Set(SENTRY_INSTRUMENTATIONS.map(i => i.module.name)));

/**
* Returns `external` with any instrumented packages removed, so a bundler that
* uses an "external" denylist (esbuild, Bun, Rollup) still bundles — and thus
* transforms — them. Matches an exact package name (`'mysql'`) or a subpath
* (`'mysql/lib/...'`); wildcard/other patterns are left untouched. `undefined`
* is returned unchanged.
*
* (Vite uses an `ssr.noExternal` allowlist instead, so it consumes
* `INSTRUMENTED_MODULE_NAMES` directly rather than this helper.)
*/
export function withoutInstrumentedExternals(external: readonly string[] | undefined): string[] | undefined {
if (!external) {
return undefined;
}
return external.filter(
entry => !INSTRUMENTED_MODULE_NAMES.some(name => entry === name || entry.startsWith(`${name}/`)),
);
}
Loading
Loading