-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(cloudflare): Auto-register Flue instrumentation in bundled workers #24476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a83bbc0
97b7ed7
143d7f5
cd785c4
30e8db6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||||||||||||||
| import { createRequire } from 'node:module'; | ||||||||||||||||||||||||||||||||||
| import { resolve } from 'node:path'; | ||||||||||||||||||||||||||||||||||
| import MagicString from 'magic-string'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Namespace binding the injected provider import uses; read back by the integration | ||||||||||||||||||||||||||||||||||
| // off the global marker. | ||||||||||||||||||||||||||||||||||
| const PROVIDER_IDENTIFIER = '__SENTRY_FLUE_RUNTIME__'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const FLUE_MODULE = '@flue/runtime'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // The bundled `@sentry/server-utils` Flue integration module (ESM build — the only one a | ||||||||||||||||||||||||||||||||||
| // worker loads). It reads `@flue/runtime` off the global marker this provider populates, | ||||||||||||||||||||||||||||||||||
| // because `instrument()` registers into module-scope state no channel payload can carry. | ||||||||||||||||||||||||||||||||||
| const FLUE_INTEGRATION_ID = /@sentry\/server-utils\/build\/esm\/integrations\/flue\.js$/; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** Whether `id` is the Sentry Flue integration module the provider injects into. */ | ||||||||||||||||||||||||||||||||||
| export function isFlueIntegrationModuleId(id: string): boolean { | ||||||||||||||||||||||||||||||||||
| const normalizedId = id.replace(/\\/g, '/').replace(/[?#].*$/, ''); | ||||||||||||||||||||||||||||||||||
| return FLUE_INTEGRATION_ID.test(normalizedId); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Splices a static `import * as … from '@flue/runtime'` into Sentry's own Flue integration module | ||||||||||||||||||||||||||||||||||
| * and exposes the namespace on the global orchestrion marker. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Flue is registered rather than patched — `instrument()` writes into module-scope state — so | ||||||||||||||||||||||||||||||||||
| * instrumenting it needs that module's own binding, and no channel payload carries one. On Node the | ||||||||||||||||||||||||||||||||||
| * user passes it by calling `instrument()` themselves; a bundled worker has no `node_modules` to | ||||||||||||||||||||||||||||||||||
| * resolve from, so it is supplied at build time instead. | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| export function sentryFlueRuntimeProviderPlugin(): { | ||||||||||||||||||||||||||||||||||
| name: string; | ||||||||||||||||||||||||||||||||||
| configResolved(config: { root: string }): void; | ||||||||||||||||||||||||||||||||||
| transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined; | ||||||||||||||||||||||||||||||||||
| } { | ||||||||||||||||||||||||||||||||||
| let providerSnippet: string | undefined; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||
| name: 'sentry-cloudflare-flue-runtime-provider', | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| configResolved(config: { root: string }): void { | ||||||||||||||||||||||||||||||||||
| // Build-time only; never ships to the worker. Probed with CJS resolution, which an ESM-only | ||||||||||||||||||||||||||||||||||
| // `@flue/runtime` fails with `ERR_PACKAGE_PATH_NOT_EXPORTED` — so only a module-not-found | ||||||||||||||||||||||||||||||||||
| // counts as absent, and any other failure still injects and lets Vite report it. Not | ||||||||||||||||||||||||||||||||||
| // `import.meta.resolve`: `parentURL` is ignored without a flag, and it is absent from the | ||||||||||||||||||||||||||||||||||
| // CJS build. | ||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| createRequire(resolve(config.root, 'noop.js')).resolve(FLUE_MODULE); | ||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||
| const code = (error as NodeJS.ErrnoException | undefined)?.code; | ||||||||||||||||||||||||||||||||||
| if (code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND') { | ||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+54
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on the comment above, it seems safer to detect
Suggested change
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, also, it'd be a bigger refactor, but I think if we have a rollup context, we can do
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this.resolve() version is def nicer, I'll do it once with the refactor |
||||||||||||||||||||||||||||||||||
| // A getter where Mastra assigns: the bundler may evaluate Sentry's module before | ||||||||||||||||||||||||||||||||||
| // `@flue/runtime` is initialized, and assigning there would store `undefined`. | ||||||||||||||||||||||||||||||||||
| providerSnippet = | ||||||||||||||||||||||||||||||||||
| `import * as ${PROVIDER_IDENTIFIER} from '${FLUE_MODULE}';\n` + | ||||||||||||||||||||||||||||||||||
| '(globalThis.__SENTRY_ORCHESTRION__ = globalThis.__SENTRY_ORCHESTRION__ || {});\n' + | ||||||||||||||||||||||||||||||||||
| '(globalThis.__SENTRY_ORCHESTRION__.providedModules = globalThis.__SENTRY_ORCHESTRION__.providedModules || {});\n' + | ||||||||||||||||||||||||||||||||||
| `Object.defineProperty(globalThis.__SENTRY_ORCHESTRION__.providedModules, '${FLUE_MODULE}', ` + | ||||||||||||||||||||||||||||||||||
| `{ configurable: true, enumerable: true, get() { return ${PROVIDER_IDENTIFIER}; } });\n`; | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| transform(code: string, id: string): { code: string; map: ReturnType<MagicString['generateMap']> } | undefined { | ||||||||||||||||||||||||||||||||||
| // `code.includes` keeps this idempotent: a second pass over already-injected output would | ||||||||||||||||||||||||||||||||||
| // otherwise emit a duplicate `import * as` binding, which is a syntax error. | ||||||||||||||||||||||||||||||||||
| if (!providerSnippet || !isFlueIntegrationModuleId(id) || code.includes(PROVIDER_IDENTIFIER)) return undefined; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const ms = new MagicString(code); | ||||||||||||||||||||||||||||||||||
| ms.prepend(providerSnippet); | ||||||||||||||||||||||||||||||||||
| return { code: ms.toString(), map: ms.generateMap({ hires: true }) }; | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+65
to
+73
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not idempotent, because the If (Note: same thing in the Mastra plugin, probably another reason to consider consolidating them.)
Suggested change
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't find a config where vite actually re-transforms its own output, but added it anyway left the mastra copy as is so it lands with the factory |
||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { join } from 'node:path'; | ||
| import { beforeAll, describe, expect, it } from 'vitest'; | ||
| import { sentryCloudflareVitePlugin } from '../../src/vite/index'; | ||
| import { isFlueIntegrationModuleId, sentryFlueRuntimeProviderPlugin } from '../../src/vite/flueRuntime'; | ||
|
|
||
| const PROVIDER_PLUGIN = 'sentry-cloudflare-flue-runtime-provider'; | ||
| const FLUE_INTEGRATION_MODULE = '/app/node_modules/@sentry/server-utils/build/esm/integrations/flue.js'; | ||
|
|
||
| /** An app root whose `node_modules` holds an ESM-only `@flue/runtime`, as published. */ | ||
| function createRootWithFlue(): string { | ||
| const root = mkdtempSync(join(tmpdir(), 'sentry-flue-root-')); | ||
| const pkgDir = join(root, 'node_modules', '@flue', 'runtime'); | ||
| mkdirSync(join(pkgDir, 'dist'), { recursive: true }); | ||
| writeFileSync( | ||
| join(pkgDir, 'package.json'), | ||
| // No `require` condition — the reason `resolve()` reports ERR_PACKAGE_PATH_NOT_EXPORTED. | ||
| JSON.stringify({ | ||
| name: '@flue/runtime', | ||
| version: '2.0.8', | ||
| type: 'module', | ||
| exports: { '.': { import: './dist/index.mjs' } }, | ||
| }), | ||
| ); | ||
| writeFileSync(join(pkgDir, 'dist', 'index.mjs'), 'export const instrument = () => {};\n'); | ||
| return root; | ||
| } | ||
|
|
||
| function createEmptyRoot(): string { | ||
| return mkdtempSync(join(tmpdir(), 'sentry-flue-empty-')); | ||
| } | ||
|
|
||
| /** An app root holding an installed but unreadable `@flue/runtime`. */ | ||
| function createRootWithBrokenFlue(): string { | ||
| const root = mkdtempSync(join(tmpdir(), 'sentry-flue-broken-')); | ||
| const pkgDir = join(root, 'node_modules', '@flue', 'runtime'); | ||
| mkdirSync(pkgDir, { recursive: true }); | ||
| writeFileSync(join(pkgDir, 'package.json'), '{ not json'); | ||
| return root; | ||
| } | ||
|
|
||
| describe('isFlueIntegrationModuleId', () => { | ||
| it('matches the ESM Flue integration module', () => { | ||
| expect(isFlueIntegrationModuleId(FLUE_INTEGRATION_MODULE)).toBe(true); | ||
| }); | ||
|
|
||
| it('ignores a trailing query/hash Vite may append', () => { | ||
| expect(isFlueIntegrationModuleId(`${FLUE_INTEGRATION_MODULE}?v=abc`)).toBe(true); | ||
| }); | ||
|
|
||
| it('normalizes Windows separators', () => { | ||
| expect( | ||
| isFlueIntegrationModuleId('C:\\app\\node_modules\\@sentry\\server-utils\\build\\esm\\integrations\\flue.js'), | ||
| ).toBe(true); | ||
| }); | ||
|
|
||
| it('does not match the CJS build (workers load ESM)', () => { | ||
| expect(isFlueIntegrationModuleId('/app/node_modules/@sentry/server-utils/build/cjs/integrations/flue.js')).toBe( | ||
| false, | ||
| ); | ||
| }); | ||
|
|
||
| it('does not match another integration module', () => { | ||
| expect(isFlueIntegrationModuleId('/app/node_modules/@sentry/server-utils/build/esm/integrations/mastra.js')).toBe( | ||
| false, | ||
| ); | ||
| }); | ||
|
|
||
| it('does not match Flue itself', () => { | ||
| expect(isFlueIntegrationModuleId('/app/node_modules/@flue/runtime/dist/index.mjs')).toBe(false); | ||
| }); | ||
| }); | ||
|
|
||
| describe('sentryFlueRuntimeProviderPlugin', () => { | ||
| describe('when the app has @flue/runtime installed', () => { | ||
| let root: string; | ||
|
|
||
| beforeAll(() => { | ||
| root = createRootWithFlue(); | ||
| }); | ||
|
|
||
| it('injects the provider even though the package is ESM-only', () => { | ||
| // Regression guard: treating that error as "absent" silently disabled auto-instrumentation. | ||
| const plugin = sentryFlueRuntimeProviderPlugin(); | ||
| plugin.configResolved({ root }); | ||
|
|
||
| const result = plugin.transform('export const x = 1;', FLUE_INTEGRATION_MODULE); | ||
|
|
||
| expect(result?.code).toContain("import * as __SENTRY_FLUE_RUNTIME__ from '@flue/runtime';"); | ||
| expect(result?.code).toContain('__SENTRY_ORCHESTRION__.providedModules'); | ||
| expect(result?.code).toContain('export const x = 1;'); | ||
| }); | ||
|
|
||
| it('exposes the namespace through a getter rather than a snapshot', () => { | ||
| const plugin = sentryFlueRuntimeProviderPlugin(); | ||
| plugin.configResolved({ root }); | ||
|
|
||
| expect(plugin.transform('', FLUE_INTEGRATION_MODULE)?.code).toContain( | ||
| 'get() { return __SENTRY_FLUE_RUNTIME__; }', | ||
| ); | ||
| }); | ||
|
|
||
| it('leaves every other module untouched', () => { | ||
| const plugin = sentryFlueRuntimeProviderPlugin(); | ||
| plugin.configResolved({ root }); | ||
|
|
||
| expect(plugin.transform('export const x = 1;', '/app/src/index.ts')).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('injects once, so a second pass cannot emit a duplicate binding', () => { | ||
| const plugin = sentryFlueRuntimeProviderPlugin(); | ||
| plugin.configResolved({ root }); | ||
|
|
||
| const once = plugin.transform('export const x = 1;', FLUE_INTEGRATION_MODULE)?.code ?? ''; | ||
|
|
||
| expect(plugin.transform(once, FLUE_INTEGRATION_MODULE)).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when @flue/runtime is installed but unresolvable', () => { | ||
| it('still injects, so the failure surfaces from Vite instead of silently disabling tracing', () => { | ||
| // Only a module-not-found means absent. Skipping on every other resolve failure is how an | ||
| // installed package silently loses instrumentation, which is the bug this plugin fixes. | ||
| const plugin = sentryFlueRuntimeProviderPlugin(); | ||
| plugin.configResolved({ root: createRootWithBrokenFlue() }); | ||
|
|
||
| expect(plugin.transform('', FLUE_INTEGRATION_MODULE)).toBeDefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('when the app does not have @flue/runtime installed', () => { | ||
| it('injects nothing', () => { | ||
| const plugin = sentryFlueRuntimeProviderPlugin(); | ||
| plugin.configResolved({ root: createEmptyRoot() }); | ||
|
|
||
| expect(plugin.transform('export const x = 1;', FLUE_INTEGRATION_MODULE)).toBeUndefined(); | ||
| }); | ||
|
|
||
| it("resolves from the app root, not from Sentry's own install", () => { | ||
| // This repo has no `@flue/runtime`, so only an app root that does can pass the check. | ||
| const withFlue = sentryFlueRuntimeProviderPlugin(); | ||
| withFlue.configResolved({ root: createRootWithFlue() }); | ||
|
|
||
| const withoutFlue = sentryFlueRuntimeProviderPlugin(); | ||
| withoutFlue.configResolved({ root: createEmptyRoot() }); | ||
|
|
||
| expect(withFlue.transform('', FLUE_INTEGRATION_MODULE)).toBeDefined(); | ||
| expect(withoutFlue.transform('', FLUE_INTEGRATION_MODULE)).toBeUndefined(); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('sentryCloudflareVitePlugin', () => { | ||
| it('always includes the Flue runtime provider plugin', () => { | ||
| expect(sentryCloudflareVitePlugin().map(plugin => plugin.name)).toContain(PROVIDER_PLUGIN); | ||
| // Not gated by auto-instrumentation: it injects into Sentry's own module, not the entry. | ||
| expect(sentryCloudflareVitePlugin({ autoInstrumentation: false }).map(plugin => plugin.name)).toContain( | ||
| PROVIDER_PLUGIN, | ||
| ); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import type { IntegrationFn } from '@sentry/core'; | ||
| import { debug, defineIntegration, GLOBAL_OBJ } from '@sentry/core'; | ||
| import type { FlueOptions } from '../ai/flue'; | ||
| import { createFlueInstrumentation } from '../ai/flue'; | ||
| import { FLUE_INTEGRATION_NAME, FLUE_MODULE_NAME } from '../ai/flue/constants'; | ||
| import { DEBUG_BUILD } from '../debug-build'; | ||
|
|
||
| type FlueInstrumentFn = (instrumentation: ReturnType<typeof createFlueInstrumentation>) => unknown; | ||
|
|
||
| /** | ||
| * The `instrument` we last registered against, so a second `setup()` in the same isolate is a | ||
| * no-op. Cloudflare runs `init()` per request, and with `cacheClient: false` that reaches here | ||
| * every time: in production Flue throws on the repeat, and under `vite dev` it disposes our | ||
| * previous registration instead — ending the turn and tool spans of every in-flight request. | ||
| * | ||
| * Keyed on the binding rather than a bare boolean so a fresh `@flue/runtime` instance (a new | ||
| * isolate reusing this module, a test swapping the marker) still registers. | ||
| */ | ||
| let registeredBinding: FlueInstrumentFn | undefined; | ||
|
|
||
| /** | ||
| * Register the instrumentation with Flue on the user's behalf, when the runtime binding is available. | ||
| * | ||
| * Flue is registered rather than patched — `instrument()` writes into module-scope state — so this | ||
| * needs a reference to that module's own binding. In a bundled worker there is no `node_modules` to | ||
| * resolve one from, so `@sentry/cloudflare/vite` splices a static `@flue/runtime` import into this | ||
| * module at build time and stashes the namespace on the global marker. Outside that setup the marker | ||
| * is empty and this no-ops, leaving the user's own `instrument(Sentry.createFlueInstrumentation())` | ||
| * as the way in. | ||
| */ | ||
| const _flueIntegration = ((options: FlueOptions = {}) => { | ||
| return { | ||
| name: FLUE_INTEGRATION_NAME, | ||
| setup() { | ||
| const provided = GLOBAL_OBJ.__SENTRY_ORCHESTRION__?.providedModules?.[FLUE_MODULE_NAME]; | ||
| const instrument = provided?.instrument as FlueInstrumentFn | undefined; | ||
|
|
||
| if (typeof instrument !== 'function') { | ||
| DEBUG_BUILD && debug.log('[Flue] no provided `@flue/runtime` binding; skipping auto-registration'); | ||
| return; | ||
| } | ||
|
|
||
| if (instrument === registeredBinding) { | ||
| DEBUG_BUILD && debug.log('[Flue] already registered in this isolate; skipping auto-registration'); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| instrument(createFlueInstrumentation(options)); | ||
| registeredBinding = instrument; | ||
| } catch (error) { | ||
| // Never rethrow: `setup()` runs inside `Sentry.init()`, which core calls unguarded and | ||
| // Cloudflare calls per request, so throwing here would take down the request handler. | ||
| if ((error as Error | undefined)?.name === 'InstrumentationAlreadyInstalledError') { | ||
| // The app owns the key and we will never win it, so stop rebuilding the instrumentation | ||
| // (two 1000-entry `LRUMap`s) on every later `init()`. | ||
| registeredBinding = instrument; | ||
| DEBUG_BUILD && debug.log('[Flue] already instrumented by the app; skipping auto-registration'); | ||
| } else { | ||
| debug.warn('[Flue] auto-registration failed; Flue spans will not be recorded:', error); | ||
| } | ||
|
RulaKhaled marked this conversation as resolved.
|
||
| } | ||
|
RulaKhaled marked this conversation as resolved.
Comment on lines
+48
to
+62
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In dev, a repeat Cloudflare calls With Under Suggesgtion: guard the call with a module-scope flag, for example
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very nice. I guarded this on the binding rather than a bare boolean, so a fresh isolate still registers |
||
| }, | ||
| }; | ||
| }) satisfies IntegrationFn; | ||
|
|
||
| export const flueIntegration = defineIntegration(_flueIntegration); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import type { InstrumentationConfig } from '../apmTypes'; | ||
| import { registrationOnly } from './registration-only'; | ||
|
|
||
| /** | ||
| * Flue publishes no diagnostics channels and needs none: it is instrumented by registering with | ||
| * `instrument()`, not by patching call sites. Transforming the entry is only how the module's | ||
| * integration gets registered at evaluation time, which is what installs it on a bundler-only SDK | ||
| * like `@sentry/cloudflare`. | ||
| */ | ||
| export const flueConfig = [ | ||
| registrationOnly({ name: '@flue/runtime', versionRange: '>=2.0.0 <3.0.0', filePath: 'dist/index.mjs' }), | ||
| ] satisfies InstrumentationConfig[]; |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Main issue/comment I'd make for this PR: this file is nearly identical to the
packages/cloudflare/src/vite/mastraObservability.tsfile, except for the module specifier, the identifier, the target regex, the tolerated resolve error, and getter versus assignment.Suggestion: extract one factory, eg
createProvidedModulePlugin({ name, moduleName, identifier, targetId, lazy }), and let both call sites shrink to a few lines. That also gives one place to fix any other concerns for both packages.Also, I notice that Mastra's plain
catch { return; }works today only because@mastra/observabilitystill publishes arequirecondition. If it goes ESM-only, that provider silently stops injecting, with the same symptom this branch just fixed for Flue. A shared check removes that potential future bug, and lets us improve both in one place.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, this can definitely be put off for a future PR, I just think we should probably get to it before there's a third one of these, and we start having a harder time deciding which drifting behavior is correct 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will take it up on a follow up. you're right about the mastra half, it isn't hypothetical either, @mastra/observability@1.17.4 still ships a require condition, so mastraObservability.ts:48's bare catch { return; } works by luck, an esm only release turns injection off silently
fixed the flue side here and left mastra untouched so both land together with the rafactor