@interfere/next 12.0.1 → 12.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -94,6 +94,54 @@ export default function RootLayout({
94
94
 
95
95
  Upgrade all `@interfere/*` SDK packages together. Keep the env var names, but replace old `int_pub_*` / `interfere_pk_*` values with the dashboard's regional public key, and replace old `ak_*` / `interfere_ak_*` build keys with the regional Interfere secret key.
96
96
 
97
+ ## Server tracing and Vercel drains
98
+
99
+ The standard instrumentation export works on Vercel and other Next.js hosts. In `full` mode, Interfere exports application spans directly. When the build config selects `drain`, the Node SDK initializes `@vercel/otel` with its default processors so Vercel can forward application spans through the configured Trace Drain. Platform spans and application spans remain separate observations.
100
+
101
+ For local development or a deployment that needs direct export despite baked drain configuration, override the mode explicitly:
102
+
103
+ ```ts
104
+ import { register as registerInterfere } from "@interfere/next/instrumentation";
105
+
106
+ export const register = () => registerInterfere({ serverTracing: "full" });
107
+ export { onRequestError } from "@interfere/next/instrumentation";
108
+ ```
109
+
110
+ The explicit option takes precedence over build-injected and environment settings. Edge always exports directly because Vercel does not forward custom Edge spans through Trace Drains. Edge reads the build-injected release identity, falling back to the configured commit SHA.
111
+
112
+ ### Composing with an existing Vercel OTel setup
113
+
114
+ Use the kit instead of calling Interfere's standalone `register()` alongside `registerOTel()`. For a Vercel deployment with a configured Trace Drain:
115
+
116
+ ```ts
117
+ import { registerOTel } from "@vercel/otel";
118
+ import { buildInterfereOtelKit } from "@interfere/next/instrumentation";
119
+
120
+ export function register() {
121
+ const kit = buildInterfereOtelKit({ serverTracing: "drain" });
122
+
123
+ if (!kit) return;
124
+
125
+ registerOTel({
126
+ serviceName: "my-app",
127
+ attributes: kit.resourceAttributes,
128
+ idGenerator: kit.idGenerator,
129
+ spanProcessors: ["auto", ...kit.spanProcessors],
130
+ logRecordProcessors: kit.logRecordProcessors,
131
+ propagators: ["auto", kit.traceContextPropagator, ...kit.propagators],
132
+ instrumentations: ["auto", ...kit.instrumentations],
133
+ });
134
+
135
+ kit.enableConsoleBridge();
136
+ }
137
+
138
+ export { onRequestError } from "@interfere/next/instrumentation";
139
+ ```
140
+
141
+ Keep `"auto"` in `spanProcessors` for Vercel drain delivery. For direct Interfere export through a customer-owned provider, use `serverTracing: "full"` and `spanProcessors: kit.spanProcessors` to choose a single export path. The host owns flushing and shutdown in composed setups.
142
+
143
+ `@vercel/otel@2.1.3` supports OTel API `>=1.9.0 <2`, stable SDK peers `>=2 <3`, and experimental peers `>=0.200.0 <0.300.0`. The SDK declares all required peers as dependencies, including `sdk-metrics`. Custom compositions must use compatible versions too.
144
+
97
145
  ## Ingest Endpoint
98
146
 
99
147
  By default the SDK posts telemetry to Interfere's endpoint. Ingest requests never carry cookies. Set `apiHost` to a subdomain of your own site that resolves to Interfere via a DNS record — this survives ad-blockers with no server-side component:
@@ -160,7 +208,7 @@ Identity is automatically cleared when the SDK is closed. Call `identity.clear()
160
208
 
161
209
  ## Consent Management
162
210
 
163
- By default, all SDK features are active. To gate features behind user consent, pass a `consent` prop to the provider:
211
+ Omitting consent preserves the SDK defaults. To deny analytics and replay from startup, pass `consent: false` to `init()` before mounting the provider. Use the provider’s `consent` prop to keep the current decision synchronized:
164
212
 
165
213
  ```tsx
166
214
  // app/layout.tsx
@@ -191,7 +239,7 @@ export default function RootLayout({
191
239
  | `analytics` | Page events, rage clicks, fingerprint | Yes |
192
240
  | `replay` | Session replay | Yes |
193
241
 
194
- Omitting the `consent` prop preserves consent supplied to `init()`, including later updates through `consent.set()`. If no initial consent was supplied, omitting the prop allows all features. Passing the prop enables only `necessary` plugins and explicitly consented categories.
242
+ Omitting the `consent` prop preserves consent supplied to `init()`, including later updates through `consent.set()`. If no initial consent was supplied, omitting the prop allows all features. Consent controls analytics and replay permission; it does not enable plugins disabled in configuration. Objects replace the current decision, and omitted categories remain allowed. Errors, logs, tracing, and session storage are not disabled by this setting.
195
243
 
196
244
  ### Imperative API
197
245
 
@@ -200,9 +248,10 @@ Use `consent.set()` and `consent.get()` from the `useInterfere` hook:
200
248
  ```tsx
201
249
  const { consent } = useInterfere();
202
250
 
203
- consent.set({ analytics: true, replay: true }); // selective
204
- consent.set(); // grant all
205
- consent.get(); // current state, or null if no gating
251
+ consent.set(false);
252
+ consent.set(true);
253
+ consent.set({ analytics: true, replay: false });
254
+ consent.get();
206
255
  ```
207
256
 
208
257
  ### Integration with consent libraries
@@ -233,7 +282,7 @@ To set consent before React renders (avoiding any window where non-consented plu
233
282
  // instrumentation-client.ts
234
283
  import { init } from "@interfere/next/instrument-client";
235
284
 
236
- init({ consent: { analytics: false, replay: false } });
285
+ await init({ consent: false });
237
286
  ```
238
287
 
239
288
  The provider's `consent` prop will then keep it in sync as the user updates their preferences.
@@ -1,10 +1,10 @@
1
1
  import { capture, span } from "@interfere/react/api";
2
2
  import { Kernel, KernelInternalOptions, KernelOptions, KernelOptions as KernelOptions$1 } from "@interfere/react/internal/kernel";
3
- import { ConsentCategory, ConsentState, GateableCategory } from "@interfere/types/sdk/plugins/manifest";
3
+ import { ConsentCategory, ConsentInput, ConsentState, GateableCategory } from "@interfere/types/sdk/plugins/manifest";
4
4
  //#region src/instrument-client.d.ts
5
5
  export declare const close: () => Promise<void>, consent: {
6
6
  get: () => import("@interfere/types/sdk/plugins/manifest").ConsentState | null;
7
- set: (value?: import("@interfere/types/sdk/plugins/manifest").ConsentState) => void;
7
+ set: (value?: import("@interfere/types/sdk/plugins/manifest").ConsentInput) => void;
8
8
  }, getKernel: () => import("@interfere/react/internal/kernel").Kernel, getKernelOrNull: () => import("@interfere/react/internal/kernel").Kernel | null, identity: {
9
9
  clear: () => void;
10
10
  get: () => import("@interfere/types/sdk/identify").IdentifyParams | null;
@@ -12,4 +12,4 @@ export declare const close: () => Promise<void>, consent: {
12
12
  }, subscribeToKernel: (listener: () => void) => () => void;
13
13
  export declare function init(opts?: KernelOptions$1): Promise<import("@interfere/react/internal/kernel").Kernel | null>;
14
14
  //#endregion
15
- export { type ConsentCategory, type ConsentState, type GateableCategory, type Kernel, type KernelInternalOptions, type KernelOptions, capture, span };
15
+ export { type ConsentCategory, type ConsentInput, type ConsentState, type GateableCategory, type Kernel, type KernelInternalOptions, type KernelOptions, capture, span };
@@ -1,124 +1,37 @@
1
+ import { PrerenderSafeIdGenerator } from "./internal/server/id-generator.mjs";
1
2
  import { ServerInstrumentationOptions } from "./internal/server/instrumentation-options.mjs";
2
3
  import { onRequestError } from "./internal/server/capture.mjs";
3
- import { PrerenderSafeIdGenerator } from "./internal/server/id-generator.mjs";
4
4
  import { AttributeValue, TextMapPropagator } from "@opentelemetry/api";
5
5
  import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
6
6
  import { Instrumentation } from "@opentelemetry/instrumentation";
7
7
  import { LogRecordProcessor } from "@opentelemetry/sdk-logs";
8
8
  //#region src/instrumentation.d.ts
9
9
  export declare function flush(): Promise<void>;
10
- /**
11
- * Composable building blocks the Interfere SDK contributes to OTel —
12
- * span / log / metric processors, baggage propagator, plus the
13
- * resource attributes and side-effect installers (console bridge +
14
- * remote-config fetch).
15
- *
16
- * Use this when you want to plug Interfere into an existing OTel
17
- * bootstrap (e.g. `@vercel/otel`'s `registerOTel({...})`, DataDog's
18
- * `dd-trace-js`, or any custom `NodeTracerProvider` setup) instead of
19
- * the standalone `register()` path. The kit holds no global state and
20
- * touches no global slots — composition is the consumer's job.
21
- *
22
- * The standalone `register()` builds the kit internally and wires it
23
- * into private providers, so customer DX stays a one-liner export.
24
- *
25
- * Returns `null` when `INTERFERE_PUBLIC_KEY` is unset — callers can spread
26
- * `kit?.spanProcessors ?? []` without a no-op guard.
27
- */
10
+ /** OTel components for a customer-owned provider. The kit does not register global providers. */
28
11
  export interface InterfereOtelKit {
29
- /**
30
- * Patches `console.{debug,log,info,warn,error}` to also emit OTel
31
- * `LogRecord`s via the global `LoggerProvider`. Caller is responsible
32
- * for ensuring a `LoggerProvider` is registered globally before
33
- * calling — `register()` handles this; for composed setups, call
34
- * after your bootstrap (`registerOTel({...})` etc.) has run. Returns
35
- * a disposer that restores the original `console.*` methods.
36
- */
12
+ /** Call after registering a global logger provider; returns a disposer that restores console methods. */
37
13
  enableConsoleBridge: () => () => void;
38
- /**
39
- * Fetches the SDK's remote plugin config and caches it for the
40
- * runtime. Customer code's `isPluginEnabled(...)` checks read from
41
- * the cache. Safe to call repeatedly.
42
- */
14
+ /** Fetches and caches remote plugin configuration. Safe to call repeatedly. */
43
15
  fetchRemoteConfig: () => Promise<void>;
44
- /**
45
- * Instrumentations the SDK ships. Currently empty — outbound HTTP
46
- * spans come from Next.js's own `patchFetch` (App Router) or
47
- * `@vercel/otel`'s fetch wrap, so we don't layer a redundant undici
48
- * instrumentation and stay off the request hot path. Kept in the
49
- * kit shape for forward-compatibility with composed setups that
50
- * spread `[...kit.instrumentations, ...]` into `registerOTel({...})`.
51
- */
16
+ idGenerator: PrerenderSafeIdGenerator;
17
+ /** Currently empty: Next.js or the host provider supplies outbound HTTP instrumentation. */
52
18
  instrumentations: Instrumentation[];
19
+ traceContextPropagator: TextMapPropagator;
53
20
  logRecordProcessors: LogRecordProcessor[];
54
21
  /**
55
- * W3C baggage propagator plus the SDK's `tracestate` freshness
56
- * stamper (guards browsers against stitching onto ISR/CDN-cached
57
- * trace context). Trace context itself is intentionally *not*
58
- * included — `@vercel/otel`'s `propagators: ["auto"]` already
59
- * registers it, and standalone `register()` adds it itself when
60
- * the global slot is empty. Spread these *after* your trace-context
61
- * propagator (e.g. `propagators: ["auto", ...kit.propagators]`) so
62
- * the freshness stamp wins the `tracestate` write.
22
+ * Baggage and freshness propagators. Compose in this order:
23
+ * `propagators: ["auto", kit.traceContextPropagator, ...kit.propagators]`.
24
+ * The cache-safe trace-context propagator suppresses cached render metadata;
25
+ * the freshness propagator must run last to stamp dynamic renders.
63
26
  */
64
27
  propagators: TextMapPropagator[];
65
- /**
66
- * Pre-built resource attribute set. Pass into your provider's
67
- * `Resource` directly, or into `@vercel/otel`'s `attributes` field
68
- * (which merges with Vercel's own auto-detected `vercel.*` attrs).
69
- */
28
+ /** Pass to the provider's resource or Vercel's `attributes` option. */
70
29
  resourceAttributes: Record<string, AttributeValue>;
71
30
  spanProcessors: SpanProcessor[];
72
31
  }
73
- /**
74
- * Pure factory — no global registration, no provider construction. Returns
75
- * the OTel primitives the SDK contributes so a host bootstrap (Vercel,
76
- * DataDog, custom) can compose them in.
77
- *
78
- * Returns `null` when `INTERFERE_PUBLIC_KEY` isn't set, so callers can
79
- * unconditionally spread `kit?.spanProcessors ?? []` in dev where the
80
- * SDK isn't configured.
81
- */
32
+ /** Builds components without registering providers. Returns null when INTERFERE_PUBLIC_KEY is unset. */
82
33
  export declare function buildInterfereOtelKit(opts?: ServerInstrumentationOptions): InterfereOtelKit | null;
83
- /**
84
- * One-line server-side bootstrap for Next.js. Customers' `instrumentation.ts`
85
- * becomes:
86
- *
87
- * ```ts
88
- * export { register } from "@interfere/next/instrumentation";
89
- * ```
90
- *
91
- * Or, with overrides:
92
- *
93
- * ```ts
94
- * import { register as base } from "@interfere/next/instrumentation";
95
- * export const register = () => base({ serviceName: "@my-org/api" });
96
- * ```
97
- *
98
- * Customers who already run another OTel bootstrap (`@vercel/otel`,
99
- * DataDog `dd-trace-js`, custom `NodeTracerProvider`) should use
100
- * `buildInterfereOtelKit({...})` instead and compose the returned
101
- * processors / readers / propagators into their existing setup —
102
- * see the kit's JSDoc for examples.
103
- *
104
- * Constructs private `NodeTracerProvider` / `LoggerProvider` /
105
- * `MeterProvider` and registers them on the global slots. Last writer
106
- * wins on the globals, so customers running another OTel bootstrap
107
- * alongside this one will end up with whichever booted last; the kit
108
- * path avoids that fight entirely.
109
- *
110
- * Wired against the Node OTel SDK (`NodeTracerProvider`,
111
- * `AsyncLocalStorageContextManager`) — so this entry only ships into the
112
- * `nodejs` runtime bundle. The Edge runtime gets the no-op stub at
113
- * `instrumentation.edge.ts` via `package.json`'s `edge-light` /
114
- * `workerd` export conditions.
115
- *
116
- * Idempotent — repeat calls (e.g. HMR) short-circuit on the
117
- * module-scoped `registered` flag.
118
- *
119
- * Bails silently when `INTERFERE_PUBLIC_KEY` is unset so dev runs without
120
- * the SDK configured don't crash.
121
- */
34
+ /** Registers standalone tracing, using Vercel's runtime export path in drain mode. Use the kit when another bootstrap owns the provider. */
122
35
  export declare function register(opts?: ServerInstrumentationOptions): Promise<void>;
123
36
  //#endregion
124
37
  export { PrerenderSafeIdGenerator, type ServerInstrumentationOptions, onRequestError };
@@ -11,12 +11,7 @@ import { InterfereOtelKit } from "./instrumentation.mjs";
11
11
  * console bridge) can't run on edge, so this builds an edge-safe pipeline
12
12
  * instead: a
13
13
  * `BasicTracerProvider` + `SimpleSpanProcessor` exporting via {@link FetchTraceExporter}.
14
- * Under `serverTracing: "drain"` the exporter is omitted and traces come from
15
- * the platform trace drain instead — the provider is still registered so
16
- * `onRequestError` can record exception events on the active span.
17
- *
18
- * Release slug is `LOCAL_DEV_RELEASE_SLUG` under force-enable (dev); a
19
- * build-baked slug for production edge is a follow-up.
14
+ * Edge always exports directly because Vercel Trace Drains do not forward custom Edge spans.
20
15
  */
21
16
  export declare function register(opts?: ServerInstrumentationOptions): Promise<void>;
22
17
  /**
@@ -1 +1 @@
1
- import{readInterfereEnv}from"./internal/env.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{FetchTraceExporter}from"./internal/server/edge-exporter.mjs";import{setServerFlush}from"./internal/server/server-flush.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{LOCAL_DEV_RELEASE_SLUG}from"@interfere/types/releases/slug";import{readServerTracingMode}from"@interfere/types/sdk/server-tracing";import{trace}from"@opentelemetry/api";import{resourceFromAttributes}from"@opentelemetry/resources";import{BasicTracerProvider,SimpleSpanProcessor}from"@opentelemetry/sdk-trace-base";let registered=!1;async function register(opts={}){if(registered)return Promise.resolve();let env=readInterfereEnv();if(!env.publicKey)return Promise.resolve();let releaseSlug=process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE?LOCAL_DEV_RELEASE_SLUG:null;releaseSlug||console.warn(`[interfere] edge runtime has no build-baked release.slug; the collector will reject edge spans until one is wired (production follow-up).`);let isDrainMode=readServerTracingMode()===`drain`;isDrainMode&&console.info(`[interfere] edge server tracing in drain mode; SDK trace exporter disabled — server traces come from the platform trace drain.`);let provider=new BasicTracerProvider({resource:resourceFromAttributes(omitUndefined({"deployment.environment.name":env.nodeEnvironment,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,"service.name":opts.serviceName??`interfere-sdk-next-edge`,"service.namespace":`interfere`,"telemetry.sdk.language":`nodejs`,"release.slug":releaseSlug||void 0})),spanProcessors:isDrainMode?[]:[new SimpleSpanProcessor(new FetchTraceExporter({baseUrl:env.apiUrl,publicKeyHeader:{apiKey:env.publicKey},headers:{"x-interfere-producer-version":PRODUCER_VERSION}}))]});return trace.setGlobalTracerProvider(provider),setServerFlush(async()=>provider.forceFlush()),registered=!0,Promise.resolve()}function buildInterfereOtelKit(_opts={}){return null}export{buildInterfereOtelKit,onRequestError,register};
1
+ import{readInterfereEnv}from"./internal/env.mjs";import{resolveReleaseSlug}from"./internal/release-slug.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{FetchTraceExporter}from"./internal/server/edge-exporter.mjs";import{setServerFlush}from"./internal/server/server-flush.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{LOCAL_DEV_RELEASE_SLUG}from"@interfere/types/releases/slug";import{getStringFromEnv}from"@opentelemetry/core";import{trace}from"@opentelemetry/api";import{defaultServiceName,resourceFromAttributes}from"@opentelemetry/resources";import{BasicTracerProvider,SimpleSpanProcessor}from"@opentelemetry/sdk-trace-base";let registered=!1;async function register(opts={}){if(registered)return Promise.resolve();let env=readInterfereEnv();if(!env.publicKey)return Promise.resolve();let releaseSlug=process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE?LOCAL_DEV_RELEASE_SLUG:resolveReleaseSlug().slug;releaseSlug||console.warn(`[interfere] edge runtime has no release.slug; configure a build release or INTERFERE_SOURCE_ID.`);let provider=new BasicTracerProvider({resource:resourceFromAttributes(omitUndefined({"deployment.environment.name":env.nodeEnvironment,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,"service.name":opts.serviceName||getStringFromEnv(`OTEL_SERVICE_NAME`)||defaultServiceName(),"service.namespace":opts.serviceNamespace,"telemetry.sdk.language":`nodejs`,"release.slug":releaseSlug||void 0})),spanProcessors:[new SimpleSpanProcessor(new FetchTraceExporter({baseUrl:env.apiUrl,publicKeyHeader:{apiKey:env.publicKey},headers:{"x-interfere-producer-version":PRODUCER_VERSION}}))]});return trace.setGlobalTracerProvider(provider),setServerFlush(async()=>provider.forceFlush()),registered=!0,Promise.resolve()}function buildInterfereOtelKit(_opts={}){return null}export{buildInterfereOtelKit,onRequestError,register};
@@ -1 +1 @@
1
- import{readInterfereEnv}from"./internal/env.mjs";import{resolveReleaseSlug}from"./internal/release-slug.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{setServerFlush}from"./internal/server/server-flush.mjs";import{fetchAndCacheRemoteConfig}from"./internal/server/remote-config.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{CacheSafeTraceContextPropagator}from"./internal/server/cache-safe-trace-context.mjs";import{bridgeConsoleToOtel}from"./internal/server/console-bridge.mjs";import{FreshnessStampingPropagator}from"./internal/server/freshness-propagator.mjs";import{installGlobalHandlers}from"./internal/server/global-handlers.mjs";import{PrerenderSafeIdGenerator}from"./internal/server/id-generator.mjs";import{drainUrl}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{readServerTracingMode}from"@interfere/types/sdk/server-tracing";import{propagation}from"@opentelemetry/api";import{resourceFromAttributes}from"@opentelemetry/resources";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{CompositePropagator,W3CBaggagePropagator}from"@opentelemetry/core";import{readLogSourcingMode}from"@interfere/types/sdk/log-sourcing";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{OTLPLogExporter}from"@opentelemetry/exporter-logs-otlp-http";import{OTLPTraceExporter}from"@opentelemetry/exporter-trace-otlp-http";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{CompressionAlgorithm}from"@opentelemetry/otlp-exporter-base";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";const EXPORT_TIMEOUT_MS=2e3;let registered=!1,tracerProvider=null,loggerProvider=null;async function flush(){await Promise.all([tracerProvider?.forceFlush(),loggerProvider?.forceFlush()])}function buildInterfereOtelKit(opts={}){let env=readInterfereEnv();if(!env.publicKey)return null;let{slug:releaseSlug}=resolveReleaseSlug();releaseSlug||console.warn("[interfere] No commit SHA available; server spans will ship without `release.slug`. Set `INTERFERE_SOURCE_ID` (or any of `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`) on the runtime env.");let tracesUrl=drainUrl(env.apiUrl,`traces`,env.publicKey),logsUrl=drainUrl(env.apiUrl,`logs`,env.publicKey),exporterHeaders={"x-interfere-producer-version":PRODUCER_VERSION},resourceAttributes=omitUndefined({"deployment.environment.name":env.nodeEnvironment,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,"service.name":opts.serviceName??`interfere-sdk-next-server`,"service.namespace":`interfere`,"telemetry.sdk.language":`nodejs`,"release.slug":releaseSlug||void 0}),compression=opts.compression===`gzip`?CompressionAlgorithm.GZIP:CompressionAlgorithm.NONE,isDrainMode=readServerTracingMode()===`drain`,spanProcessors=[new BaggageSpanProcessor(()=>!0),...isDrainMode?[]:[new BatchSpanProcessor(new OTLPTraceExporter({compression,headers:exporterHeaders,timeoutMillis:EXPORT_TIMEOUT_MS,url:tracesUrl}))],...opts._internalAdditionalSpanProcessors??[]],isLogDrainMode=readLogSourcingMode()===`drain`;return{enableConsoleBridge:()=>isLogDrainMode?()=>{}:bridgeConsoleToOtel(),fetchRemoteConfig:async()=>fetchAndCacheRemoteConfig(),instrumentations:[],logRecordProcessors:[...isLogDrainMode?[]:[new BatchLogRecordProcessor({exporter:new OTLPLogExporter({compression,headers:exporterHeaders,timeoutMillis:EXPORT_TIMEOUT_MS,url:logsUrl})})],...opts._internalAdditionalLogRecordProcessors??[]],propagators:[new FreshnessStampingPropagator,new W3CBaggagePropagator],resourceAttributes,spanProcessors}}async function register(opts={}){if(registered)return;let kit=buildInterfereOtelKit(opts);if(!kit)return;readServerTracingMode()===`drain`&&console.info(`[interfere] server tracing in drain mode; SDK trace exporter disabled — server traces come from the platform trace drain.`);let resource=resourceFromAttributes(kit.resourceAttributes);tracerProvider=new NodeTracerProvider({idGenerator:new PrerenderSafeIdGenerator,resource,spanProcessors:kit.spanProcessors}),propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new CacheSafeTraceContextPropagator,...kit.propagators]})),tracerProvider.register({contextManager:new AsyncLocalStorageContextManager().enable()}),loggerProvider=new LoggerProvider({processors:kit.logRecordProcessors,resource});let{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider),registerInstrumentations({instrumentations:kit.instrumentations,tracerProvider}),opts.consoleBridge!==!1&&kit.enableConsoleBridge(),installGlobalHandlers({captureUncaughtException:opts.captureUncaughtException??!0,captureUnhandledRejection:opts.captureUnhandledRejection??!0,exitOnUncaughtException:process.env.NODE_ENV===`production`,flush}),setServerFlush(flush),registered=!0}export{PrerenderSafeIdGenerator,buildInterfereOtelKit,flush,onRequestError,register};
1
+ import{readInterfereEnv}from"./internal/env.mjs";import{resolveReleaseSlug}from"./internal/release-slug.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{setServerFlush}from"./internal/server/server-flush.mjs";import{fetchAndCacheRemoteConfig}from"./internal/server/remote-config.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{CacheSafeTraceContextPropagator}from"./internal/server/cache-safe-trace-context.mjs";import{bridgeConsoleToOtel}from"./internal/server/console-bridge.mjs";import{FreshnessStampingPropagator}from"./internal/server/freshness-propagator.mjs";import{installGlobalHandlers}from"./internal/server/global-handlers.mjs";import{PrerenderSafeIdGenerator}from"./internal/server/id-generator.mjs";import{drainUrl}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{CompositePropagator,W3CBaggagePropagator,getStringFromEnv}from"@opentelemetry/core";import{ProxyTracerProvider,propagation,trace}from"@opentelemetry/api";import{defaultServiceName,resourceFromAttributes}from"@opentelemetry/resources";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{readLogSourcingMode}from"@interfere/types/sdk/log-sourcing";import{readServerTracingMode}from"@interfere/types/sdk/server-tracing";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{OTLPLogExporter}from"@opentelemetry/exporter-logs-otlp-http";import{OTLPTraceExporter}from"@opentelemetry/exporter-trace-otlp-http";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{CompressionAlgorithm}from"@opentelemetry/otlp-exporter-base";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";const EXPORT_TIMEOUT_MS=2e3;function canFlush(provider){return`forceFlush`in provider&&typeof provider.forceFlush==`function`}let registered=!1,tracerProvider=null,loggerProvider=null;async function flush(){await Promise.all([tracerProvider?.forceFlush(),loggerProvider?.forceFlush()])}function buildInterfereOtelKit(opts={}){let env=readInterfereEnv();if(!env.publicKey)return null;let{slug:releaseSlug}=resolveReleaseSlug();releaseSlug||console.warn("[interfere] No commit SHA available; server spans will ship without `release.slug`. Set `INTERFERE_SOURCE_ID` (or any of `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`) on the runtime env.");let tracesUrl=drainUrl(env.apiUrl,`traces`,env.publicKey),logsUrl=drainUrl(env.apiUrl,`logs`,env.publicKey),exporterHeaders={"x-interfere-producer-version":PRODUCER_VERSION},resourceAttributes=omitUndefined({"deployment.environment.name":env.nodeEnvironment,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,"service.name":opts.serviceName||getStringFromEnv(`OTEL_SERVICE_NAME`)||defaultServiceName(),"service.namespace":opts.serviceNamespace,"telemetry.sdk.language":`nodejs`,"release.slug":releaseSlug||void 0}),compression=opts.compression===`gzip`?CompressionAlgorithm.GZIP:CompressionAlgorithm.NONE,isDrainMode=(opts.serverTracing??readServerTracingMode())===`drain`,spanProcessors=[new BaggageSpanProcessor(()=>!0),...isDrainMode?[]:[new BatchSpanProcessor(new OTLPTraceExporter({compression,headers:exporterHeaders,timeoutMillis:EXPORT_TIMEOUT_MS,url:tracesUrl}))],...opts._internalAdditionalSpanProcessors??[]],isLogDrainMode=readLogSourcingMode()===`drain`,logRecordProcessors=[...isLogDrainMode?[]:[new BatchLogRecordProcessor({exporter:new OTLPLogExporter({compression,headers:exporterHeaders,timeoutMillis:EXPORT_TIMEOUT_MS,url:logsUrl})})],...opts._internalAdditionalLogRecordProcessors??[]];return{enableConsoleBridge:()=>isLogDrainMode?()=>{}:bridgeConsoleToOtel(),fetchRemoteConfig:async()=>fetchAndCacheRemoteConfig(),idGenerator:new PrerenderSafeIdGenerator,instrumentations:[],traceContextPropagator:new CacheSafeTraceContextPropagator,logRecordProcessors,propagators:[new FreshnessStampingPropagator,new W3CBaggagePropagator],resourceAttributes,spanProcessors}}async function register(opts={}){if(registered)return;let drainMode=(opts.serverTracing??readServerTracingMode())===`drain`;if(drainMode&&process.env.OTEL_SDK_DISABLED?.trim())return;let kit=buildInterfereOtelKit(opts);if(!kit)return;let resource=resourceFromAttributes(kit.resourceAttributes);if(drainMode){let{registerOTel}=await import(`@vercel/otel`);registerOTel({serviceName:String(kit.resourceAttributes[`service.name`]),attributes:kit.resourceAttributes,idGenerator:kit.idGenerator,instrumentations:kit.instrumentations,propagators:[`auto`,kit.traceContextPropagator,...kit.propagators],spanProcessors:[`auto`,...kit.spanProcessors]});let provider=trace.getTracerProvider(),delegate=provider instanceof ProxyTracerProvider?provider.getDelegate():provider;if(!canFlush(delegate))throw Error(`Interfere could not register the Vercel tracer provider. Use buildInterfereOtelKit with an existing OpenTelemetry bootstrap.`);tracerProvider=delegate}else{let provider=new NodeTracerProvider({idGenerator:kit.idGenerator,resource,spanProcessors:kit.spanProcessors});propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[kit.traceContextPropagator,...kit.propagators]})),provider.register({contextManager:new AsyncLocalStorageContextManager().enable()}),tracerProvider=provider}loggerProvider=new LoggerProvider({processors:kit.logRecordProcessors,resource});let{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider),registerInstrumentations({instrumentations:kit.instrumentations,tracerProvider:trace.getTracerProvider()}),opts.consoleBridge!==!1&&kit.enableConsoleBridge(),installGlobalHandlers({captureUncaughtException:opts.captureUncaughtException??!0,captureUnhandledRejection:opts.captureUnhandledRejection??!0,exitOnUncaughtException:process.env.NODE_ENV===`production`,flush}),setServerFlush(flush),registered=!0}export{PrerenderSafeIdGenerator,buildInterfereOtelKit,flush,onRequestError,register};
@@ -1,2 +1,2 @@
1
- import{omitUndefined}from"@interfere/helpers/omit-undefined";import{INGEST_URL_GLOBAL}from"@interfere/types/sdk/injected-globals";import{SERVER_TRACING_GLOBAL}from"@interfere/types/sdk/server-tracing";import{PLUGINS_GLOBAL}from"@interfere/types/sdk/remote-config";import{LOG_SOURCING_GLOBAL}from"@interfere/types/sdk/log-sourcing";import{callbackify}from"node:util";import{createBuildConfigResolver}from"@interfere/build/config";const resolveBuildConfig=createBuildConfigResolver();function valueInjectionLoader(userCode){let options=this.getOptions(),callback=this.async(),values=resolveValues(options);callbackify(async()=>{let globals=await resolveRemoteConfig(resolveFetch(options)).catch(()=>({}));return render({...values,...globals},userCode)})(callback)}function remoteGlobals(config){return omitUndefined({[SERVER_TRACING_GLOBAL]:config.serverTracing,[LOG_SOURCING_GLOBAL]:config.logSourcing,[INGEST_URL_GLOBAL]:config.ingestUrl,[PLUGINS_GLOBAL]:config.plugins})}function render(values,userCode){let lines=Object.entries(values).map(([key,value])=>`globalThis[${JSON.stringify(key)}] = ${JSON.stringify(value)};`);return lines.length===0?userCode:`${lines.join(`
1
+ import{omitUndefined}from"@interfere/helpers/omit-undefined";import{INGEST_URL_GLOBAL}from"@interfere/types/sdk/injected-globals";import{PLUGINS_GLOBAL}from"@interfere/types/sdk/remote-config";import{LOG_SOURCING_GLOBAL}from"@interfere/types/sdk/log-sourcing";import{SERVER_TRACING_GLOBAL}from"@interfere/types/sdk/server-tracing";import{callbackify}from"node:util";import{createBuildConfigResolver}from"@interfere/build/config";const resolveBuildConfig=createBuildConfigResolver();function valueInjectionLoader(userCode){let options=this.getOptions(),callback=this.async(),values=resolveValues(options);callbackify(async()=>{let globals=await resolveRemoteConfig(resolveFetch(options)).catch(()=>({}));return render({...values,...globals},userCode)})(callback)}function remoteGlobals(config){return omitUndefined({[SERVER_TRACING_GLOBAL]:config.serverTracing,[LOG_SOURCING_GLOBAL]:config.logSourcing,[INGEST_URL_GLOBAL]:config.ingestUrl,[PLUGINS_GLOBAL]:config.plugins})}function render(values,userCode){let lines=Object.entries(values).map(([key,value])=>`globalThis[${JSON.stringify(key)}] = ${JSON.stringify(value)};`);return lines.length===0?userCode:`${lines.join(`
2
2
  `)}\n${userCode}`}async function resolveRemoteConfig(fetchOptions){return fetchOptions?resolveBuildConfig(fetchOptions.apiUrl,fetchOptions.publicKey,fetchOptions.cacheScope).then(remoteGlobals):{}}function parseJson(text){if(typeof text==`string`)try{return JSON.parse(text)}catch{return}}function resolveFetch(options){let parsed=parseJson(options.serializedFetch);return isRemoteConfigFetch(parsed)?parsed:null}function isRemoteConfigFetch(value){return isRecord(value)&&typeof value.apiUrl==`string`&&typeof value.publicKey==`string`&&(value.cacheScope===void 0||typeof value.cacheScope==`string`)}function resolveValues(options){let parsed=parseJson(options.serializedValues);return isRecord(parsed)?parsed:{}}function isRecord(value){return typeof value==`object`&&!!value&&!Array.isArray(value)}export{valueInjectionLoader as default};
@@ -1,5 +1,5 @@
1
- import { Context, TextMapSetter } from "@opentelemetry/api";
2
1
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
2
+ import { Context, TextMapSetter } from "@opentelemetry/api";
3
3
  //#region src/internal/server/cache-safe-trace-context.d.ts
4
4
  export interface CacheSafeTraceContextPropagatorOptions {
5
5
  isStaticRender?: () => boolean;
@@ -1 +1 @@
1
- import{isStaticGenerationRender}from"./static-generation.mjs";import{W3CTraceContextPropagator}from"@opentelemetry/core";var CacheSafeTraceContextPropagator=class extends W3CTraceContextPropagator{isStaticRender;constructor(options={}){super(),this.isStaticRender=options.isStaticRender??isStaticGenerationRender}inject(context,carrier,setter){Array.isArray(carrier)&&this.isStaticRender()||super.inject(context,carrier,setter)}};export{CacheSafeTraceContextPropagator};
1
+ import{isStaticGenerationRender}from"./static-generation.mjs";import{W3CTraceContextPropagator}from"@opentelemetry/core";import{z}from"zod";const metadataEntrySchema=z.object({key:z.string()});var CacheSafeTraceContextPropagator=class extends W3CTraceContextPropagator{isStaticRender;constructor(options={}){super(),this.isStaticRender=options.isStaticRender??isStaticGenerationRender}inject(context,carrier,setter){if(Array.isArray(carrier)){for(let index=carrier.length-1;index>=0;index--){let entry=metadataEntrySchema.safeParse(carrier[index]);entry.success&&(entry.data.key===`traceparent`||entry.data.key===`tracestate`)&&carrier.splice(index,1)}if(this.isStaticRender())return}super.inject(context,carrier,setter)}};export{CacheSafeTraceContextPropagator};
@@ -1,6 +1,6 @@
1
+ import { ExportResult } from "@opentelemetry/core";
1
2
  import { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
2
3
  import { InterfereClient } from "@interfere/javascript";
3
- import { ExportResult } from "@opentelemetry/core";
4
4
  //#region src/internal/server/edge-exporter.d.ts
5
5
  export declare class FetchTraceExporter implements SpanExporter {
6
6
  private readonly client;
@@ -1 +1 @@
1
- import{InterfereClient}from"@interfere/javascript";import{toError}from"@interfere/types/sdk/errors";import{ExportResultCode}from"@opentelemetry/core";import{JsonTraceSerializer}from"@opentelemetry/otlp-transformer";import{z}from"zod";var FetchTraceExporter=class{client;constructor(options){this.client=new InterfereClient({...options,maxRetries:0,fetch:Object.assign(async(url,init)=>globalThis.fetch(url,{...init,keepalive:!0}),globalThis.fetch)})}export(spans,resultCallback){if(spans.length===0){resultCallback({code:ExportResultCode.SUCCESS});return}let payload=JsonTraceSerializer.serializeRequest(spans);if(!payload||payload.byteLength===0){resultCallback({code:ExportResultCode.SUCCESS});return}(async()=>{try{let body=z.record(z.string(),z.unknown()).parse(JSON.parse(new TextDecoder().decode(payload)));await this.client.drain.otlpExport({signal:`traces`,body}),resultCallback({code:ExportResultCode.SUCCESS})}catch(error){resultCallback({code:ExportResultCode.FAILED,error:toError(error)})}})()}async shutdown(){return Promise.resolve()}async forceFlush(){return Promise.resolve()}};export{FetchTraceExporter};
1
+ import{ExportResultCode}from"@opentelemetry/core";import{InterfereClient}from"@interfere/javascript";import{toError}from"@interfere/types/sdk/errors";import{JsonTraceSerializer}from"@opentelemetry/otlp-transformer";import{z}from"zod";var FetchTraceExporter=class{client;constructor(options){this.client=new InterfereClient({...options,maxRetries:0,fetch:Object.assign(async(url,init)=>globalThis.fetch(url,{...init,keepalive:!0}),globalThis.fetch)})}export(spans,resultCallback){if(spans.length===0){resultCallback({code:ExportResultCode.SUCCESS});return}let payload=JsonTraceSerializer.serializeRequest(spans);if(!payload||payload.byteLength===0){resultCallback({code:ExportResultCode.SUCCESS});return}(async()=>{try{let body=z.record(z.string(),z.unknown()).parse(JSON.parse(new TextDecoder().decode(payload)));await this.client.drain.otlpExport({signal:`traces`,body}),resultCallback({code:ExportResultCode.SUCCESS})}catch(error){resultCallback({code:ExportResultCode.FAILED,error:toError(error)})}})()}async shutdown(){return Promise.resolve()}async forceFlush(){return Promise.resolve()}};export{FetchTraceExporter};
@@ -1 +1 @@
1
- import{isStaticGenerationRender}from"./static-generation.mjs";import{isSpanContextValid,trace}from"@opentelemetry/api";import{TraceState,isTracingSuppressed}from"@opentelemetry/core";import{TRACESTATE_FRESHNESS_KEY,freshnessStamp}from"@interfere/types/sdk/trace-freshness";const TRACE_STATE_HEADER=`tracestate`;function carrierHoldsTraceparent(carrier){return carrier.some(entry=>typeof entry==`object`&&!!entry&&`key`in entry&&entry.key===`traceparent`)}var FreshnessStampingPropagator=class{isStaticRender;constructor(options={}){this.isStaticRender=options.isStaticRender??isStaticGenerationRender}inject(context,carrier,setter){let spanContext=trace.getSpanContext(context);if(!spanContext||!isSpanContextValid(spanContext)||isTracingSuppressed(context)||Array.isArray(carrier)&&this.isStaticRender()&&!carrierHoldsTraceparent(carrier))return;let traceState=(spanContext.traceState??new TraceState).set(TRACESTATE_FRESHNESS_KEY,freshnessStamp(Date.now()));setter.set(carrier,TRACE_STATE_HEADER,traceState.serialize())}extract(context){return context}fields(){return[TRACE_STATE_HEADER]}};export{FreshnessStampingPropagator};
1
+ import{isStaticGenerationRender}from"./static-generation.mjs";import{TraceState,isTracingSuppressed}from"@opentelemetry/core";import{isSpanContextValid,trace}from"@opentelemetry/api";import{TRACESTATE_FRESHNESS_KEY,freshnessStamp}from"@interfere/types/sdk/trace-freshness";const TRACE_STATE_HEADER=`tracestate`;function carrierHoldsTraceparent(carrier){return carrier.some(entry=>typeof entry==`object`&&!!entry&&`key`in entry&&entry.key===`traceparent`)}var FreshnessStampingPropagator=class{isStaticRender;constructor(options={}){this.isStaticRender=options.isStaticRender??isStaticGenerationRender}inject(context,carrier,setter){let spanContext=trace.getSpanContext(context);if(!spanContext||!isSpanContextValid(spanContext)||isTracingSuppressed(context)||Array.isArray(carrier)&&this.isStaticRender()&&!carrierHoldsTraceparent(carrier))return;let traceState=(spanContext.traceState??new TraceState).set(TRACESTATE_FRESHNESS_KEY,freshnessStamp(Date.now()));setter.set(carrier,TRACE_STATE_HEADER,traceState.serialize())}extract(context){return context}fields(){return[TRACE_STATE_HEADER]}};export{FreshnessStampingPropagator};
@@ -58,9 +58,13 @@ export interface ServerInstrumentationOptions {
58
58
  */
59
59
  consoleBridge?: boolean;
60
60
  /**
61
- * Override the OTel `service.name` resource attribute. Defaults to
62
- * `"interfere-sdk-next-server"`.
61
+ * Application service name. Defaults to `OTEL_SERVICE_NAME`, then OTel's
62
+ * `unknown_service:<executable>` or `unknown_service` when unavailable.
63
63
  */
64
64
  serviceName?: string;
65
+ /** Optional application namespace for grouping related services. */
66
+ serviceNamespace?: string;
67
+ /** Overrides build-injected and environment tracing settings. Use full for direct export during local development. */
68
+ serverTracing?: "full" | "drain";
65
69
  }
66
70
  //#endregion
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/next`,version=`12.0.1`;export{name,version};
1
+ var name=`@interfere/next`,version=`12.0.3`;export{name,version};
@@ -1,9 +1,9 @@
1
1
  import { PluginOverrides, useInterfere, useSession } from "@interfere/react/provider";
2
2
  import { PropsWithChildren } from "react";
3
- import { ConsentCategory, ConsentState, ConsentState as ConsentState$1, GateableCategory } from "@interfere/types/sdk/plugins/manifest";
3
+ import { ConsentCategory, ConsentInput, ConsentInput as ConsentInput$1, ConsentState, GateableCategory } from "@interfere/types/sdk/plugins/manifest";
4
4
  //#region src/provider.d.ts
5
5
  interface InterfereProviderProps extends PropsWithChildren {
6
- consent?: ConsentState$1 | undefined;
6
+ consent?: ConsentInput$1 | undefined;
7
7
  errorBoundary?: boolean;
8
8
  plugins?: PluginOverrides | undefined;
9
9
  }
@@ -17,4 +17,4 @@ interface InterfereProviderProps extends PropsWithChildren {
17
17
  */
18
18
  export declare function InterfereProvider({ children, consent, plugins, errorBoundary }: InterfereProviderProps): import("react").JSX.Element;
19
19
  //#endregion
20
- export { type ConsentCategory, type ConsentState, type GateableCategory, useInterfere, useSession };
20
+ export { type ConsentCategory, type ConsentInput, type ConsentState, type GateableCategory, useInterfere, useSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/next",
3
- "version": "12.0.1",
3
+ "version": "12.0.3",
4
4
  "description": "Build software that never breaks.",
5
5
  "keywords": [
6
6
  "error-tracking",
@@ -24,7 +24,9 @@
24
24
  "dist"
25
25
  ],
26
26
  "type": "module",
27
- "sideEffects": false,
27
+ "sideEffects": [
28
+ "./dist/instrumentation-client.mjs"
29
+ ],
28
30
  "exports": {
29
31
  "./config": {
30
32
  "monorepo": "./src/config.ts",
@@ -89,17 +91,17 @@
89
91
  },
90
92
  "scripts": {
91
93
  "build": "tsdown",
92
- "test": "vitest run",
94
+ "test": "vitest run --config ../../../../vitest.config.ts",
93
95
  "test:e2e": "playwright test",
94
- "typecheck": "tsc --noEmit"
96
+ "typecheck": "bun check"
95
97
  },
96
98
  "dependencies": {
97
- "@interfere/build": "^0.2.1",
98
- "@interfere/constants": "^9.1.0",
99
+ "@interfere/build": "^0.2.2",
100
+ "@interfere/constants": "^9.2.0",
99
101
  "@interfere/helpers": "^9.2.0",
100
102
  "@interfere/javascript": "^0.2.1",
101
- "@interfere/react": "^12.0.1",
102
- "@interfere/types": "^10.2.0",
103
+ "@interfere/react": "^12.0.3",
104
+ "@interfere/types": "^10.2.1",
103
105
  "@opentelemetry/api": "^1.9.1",
104
106
  "@opentelemetry/api-logs": "^0.222.0",
105
107
  "@opentelemetry/baggage-span-processor": "^0.5.0",
@@ -112,12 +114,15 @@
112
114
  "@opentelemetry/otlp-transformer": "^0.222.0",
113
115
  "@opentelemetry/resources": "^2.11.0",
114
116
  "@opentelemetry/sdk-logs": "^0.222.0",
117
+ "@opentelemetry/sdk-metrics": "^2.11.0",
115
118
  "@opentelemetry/sdk-trace-base": "^2.11.0",
116
119
  "@opentelemetry/sdk-trace-node": "^2.11.0",
120
+ "@vercel/otel": "2.1.3",
117
121
  "chalk": "^6.0.0",
118
122
  "zod": "^4.5.4"
119
123
  },
120
124
  "devDependencies": {
125
+ "@interfere/test-utils": "^10.0.0",
121
126
  "@interfere/typescript-config": "^9.0.2",
122
127
  "@interfere/vitest-config": "0.0.0",
123
128
  "@playwright/test": "^1.63.0",