@interfere/next 12.0.4 → 12.1.0

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.
Files changed (37) hide show
  1. package/README.md +109 -5
  2. package/dist/config.d.mts +7 -21
  3. package/dist/config.mjs +1 -1
  4. package/dist/instrument-client.mjs +1 -1
  5. package/dist/instrumentation.edge.mjs +1 -1
  6. package/dist/instrumentation.mjs +1 -1
  7. package/dist/internal/build/configure-build.d.mts +1 -0
  8. package/dist/internal/build/pipeline.d.mts +9 -0
  9. package/dist/internal/build/pipeline.mjs +1 -1
  10. package/dist/internal/build/release/index.d.mts +60 -46
  11. package/dist/internal/build/release/index.mjs +1 -1
  12. package/dist/internal/build/source-maps/discover-turbopack.d.mts +1 -0
  13. package/dist/internal/build/source-maps/discover-turbopack.mjs +1 -1
  14. package/dist/internal/build/source-maps/discover-webpack.d.mts +1 -0
  15. package/dist/internal/build/source-maps/discover-webpack.mjs +1 -1
  16. package/dist/internal/build/source-maps/discover.d.mts +2 -0
  17. package/dist/internal/build/source-maps/guard.d.mts +1 -1
  18. package/dist/internal/build/source-maps/guard.mjs +1 -1
  19. package/dist/internal/build/value-injection-loader.mjs +1 -1
  20. package/dist/internal/build-marker-guard.d.mts +1 -0
  21. package/dist/internal/build-marker-guard.mjs +1 -1
  22. package/dist/internal/console-output.d.mts +3 -0
  23. package/dist/internal/console-output.mjs +1 -0
  24. package/dist/internal/env.mjs +1 -1
  25. package/dist/internal/logger.mjs +1 -1
  26. package/dist/internal/server/counter-id-generator.d.mts +12 -0
  27. package/dist/internal/server/counter-id-generator.mjs +1 -0
  28. package/dist/internal/server/edge-exporter.mjs +1 -1
  29. package/dist/internal/server/id-generator.d.mts +2 -28
  30. package/dist/internal/server/id-generator.mjs +1 -1
  31. package/dist/internal/server/instrumentation-options.d.mts +12 -0
  32. package/dist/internal/server/trace-payload.d.mts +5 -0
  33. package/dist/internal/server/trace-payload.mjs +1 -0
  34. package/dist/package.mjs +1 -1
  35. package/dist/provider.d.mts +4 -1
  36. package/dist/provider.mjs +1 -1
  37. package/package.json +8 -8
package/README.md CHANGED
@@ -82,13 +82,54 @@ export default function RootLayout({
82
82
  }
83
83
  ```
84
84
 
85
+ `InterfereProvider` captures render errors by default and rethrows them so your host error boundary keeps control of the fallback. Set `captureRenderErrors={false}` to disable this capture. The deprecated `errorBoundary` prop remains supported; `captureRenderErrors` takes precedence when both are set.
86
+
85
87
  ### Environment Variables
86
88
 
87
- | Variable | Required | Description |
88
- | ---------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
89
- | `INTERFERE_PUBLIC_KEY` | Yes | Surface public key in the `interfere_public_us_*` or `interfere_public_eu_*` format. Public by design; used for ingestion. |
90
- | `INTERFERE_API_KEY` | Yes for release metadata | Interfere secret key in the `interfere_secret_us_*` or `interfere_secret_eu_*` format. Server-side build credential; never expose in browser code. |
91
- | `INTERFERE_API_URL` | No | Override the ingest endpoint in `direct` mode (e.g. a regional endpoint). Defaults to Interfere's endpoint. |
89
+ | Variable | Required | Description |
90
+ | ---------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `INTERFERE_PUBLIC_KEY` | Yes | Surface public key in the `interfere_public_us_*` or `interfere_public_eu_*` format. Public by design; used for ingestion. A build without it logs a warning and sends no events. |
92
+ | `INTERFERE_API_KEY` | Yes for release metadata | Interfere secret key in the `interfere_secret_us_*` or `interfere_secret_eu_*` format. Server-side build credential; never expose in browser code. A build without it skips the release. |
93
+ | `INTERFERE_SOURCEMAP_UPLOAD_CONCURRENCY` | No | Maximum concurrent source-map uploads. Defaults to 20; must be a positive integer. |
94
+ | `INTERFERE_API_URL` | No | Override the ingest endpoint in `direct` mode (e.g. a regional endpoint). Defaults to Interfere's endpoint. |
95
+
96
+ ### Connect releases to Vercel
97
+
98
+ In Interfere, connect Vercel and link this Surface to its Vercel project. Also link the Surface to its source repository. These links let the build register releases and upload source maps; installing the SDK does not select a project for you.
99
+
100
+ An incomplete Surface setup produces a build warning and skips release registration and source-map upload. It does not fail the application build or disable SDK monitoring. Source-map cleanup still follows your `purge` setting. Complete the links and redeploy to enable release attribution and source-mapped stack traces.
101
+
102
+ For Vercel deployments, choose the Vercel project rather than CLI as the deployment target. CLI registers a generic deployment and does not provide the Vercel project mapping used for deployment events and drains.
103
+
104
+ ### Source-map uploads and cleanup
105
+
106
+ Source-map uploads and cleanup are enabled by default. To upload only in production, select the deployment environment explicitly:
107
+
108
+ ```ts
109
+ import { withInterfere } from "@interfere/next/config";
110
+
111
+ export default withInterfere(
112
+ {
113
+ sourceMaps: {
114
+ upload: process.env.INTERFERE_ENVIRONMENT === "production",
115
+ purge: true,
116
+ },
117
+ },
118
+ nextConfig,
119
+ );
120
+ ```
121
+
122
+ Set `INTERFERE_ENVIRONMENT` to the deployment environment, such as `staging` or `production`. `NODE_ENV` is usually `production` for both staging and production builds, so it cannot distinguish them.
123
+
124
+ With `upload: false`, the SDK still registers the release and retains browser/server monitoring. It skips source-map signing, upload, and finalization requests. Stack traces cannot be resolved to original source in environments without uploaded maps. Release registration still requires the build API key.
125
+
126
+ `purge` controls cleanup independently. When true, the SDK deletes discovered maps and removes their source-map references after the pipeline, including upload failures or intentionally disabled uploads. Cleanup failures are reported separately without replacing the original pipeline error. Set `purge: false` to retain maps for another uploader. Setting both options to false also skips source-map discovery in the build hook.
127
+
128
+ A production build registers a release when `INTERFERE_API_KEY` is set, so set it on builds that you deploy. Without it, the build logs a warning and skips the release. `upload: false` skips only the source maps; it does not skip the release.
129
+
130
+ To turn Interfere off for a build, pass `withInterfere({ disable: true }, nextConfig)`. The build skips the release, the source-map upload and every Interfere check, and the browser and server SDKs send no events.
131
+
132
+ Only configuration that is wrong fails a production build: an `INTERFERE_API_KEY` with the wrong prefix, a missing commit SHA when the key is set, an invalid `apiHost`, or a public key without an `instrumentation-client` file. A missing `INTERFERE_PUBLIC_KEY` logs a warning. Errors from the Interfere API, including a rejected secret key, are logged and do not fail the build.
92
133
 
93
134
  ### Migrating From 0.x
94
135
 
@@ -291,6 +332,69 @@ await init({ consent: false });
291
332
 
292
333
  The provider's `consent` prop will then keep it in sync as the user updates their preferences.
293
334
 
335
+ ## Privacy
336
+
337
+ ### Private elements
338
+
339
+ Mark elements whose content the SDK never records in browser `init()`. The selectors apply to session replay and to click, submit and rage-click records from the first capture, even when replay is disabled:
340
+
341
+ ```ts
342
+ // instrumentation-client.ts
343
+ import { init } from "@interfere/next/instrument-client";
344
+
345
+ await init({
346
+ privacy: { blockSelector: "[data-sensitive]" },
347
+ });
348
+ ```
349
+
350
+ The `interfere-block` and `interfere-mask` classes always apply. Interaction records keep the event, tag and field label of a private target, never its text or value. `replay.blockSelector` and `replay.maskTextSelector` on the provider apply to replay only. `<InterfereProvider plugins={{ pageEvents: { interactions: false } }}>` drops click and submit spans but keeps navigation, document-load and resource timing. The [`@interfere/react` README](https://www.npmjs.com/package/@interfere/react#privacy) lists every recorded field.
351
+
352
+ ### URL redaction
353
+
354
+ The browser SDK and the server kit always redact URL credentials, and redact the values of built-in sensitive query parameters (such as `token`, `password`, `sig` and `X-Amz-Signature`) by default. Parameter names stay, and redacted values become `REDACTED`. Use `privacy.urls` in browser `init()` and in the server kit or `register()` (Node and Edge) to change this:
355
+
356
+ ```ts
357
+ // instrumentation-client.ts
358
+ import { init } from "@interfere/next/instrument-client";
359
+
360
+ await init({ privacy: { urls: { allow: ["page", "sort"] } } });
361
+ ```
362
+
363
+ ```ts
364
+ // instrumentation.ts
365
+ import { buildInterfereOtelKit } from "@interfere/next/instrumentation";
366
+
367
+ const kit = buildInterfereOtelKit({ privacy: { urls: { deny: ["email"] } } });
368
+ ```
369
+
370
+ `{ allow }` redacts every value except the listed names. `{ deny }` adds names to the built-in list. Both keep fragments unless you pass `fragment: true`. `true` redacts every value and removes fragments. `false` keeps query values. Requests, routing and trace propagation keep the original URL. The [`@interfere/react` README](https://www.npmjs.com/package/@interfere/react#url-redaction) lists the built-in names and which records are covered.
371
+
372
+ On the server, the kit's processor redacts each span while it ends. Every processor and exporter on the same provider then receives the redacted span, including Vercel's `"auto"` drain processors and any second exporter. Because sensitive-name redaction is on by default, those exporters now receive `REDACTED` for the built-in names too; pass `privacy: { urls: false }` to keep query values. Providers built on `@opentelemetry/sdk-trace-base` older than 2.3 lack the `onEnding` hook; there the SDK redacts attributes and exception events in `onEnd`, which only reaches processors listed after the kit's and leaves span names and status messages unchanged.
373
+
374
+ ## Browser span processors
375
+
376
+ Pass standard OpenTelemetry processors to browser `init()` in `instrumentation-client.ts`:
377
+
378
+ ```ts
379
+ import { init } from "@interfere/next/instrument-client";
380
+ import type { SpanProcessor } from "@opentelemetry/sdk-trace-base";
381
+
382
+ const dropTenant: SpanProcessor = {
383
+ onStart() {},
384
+ onEnd(span) {
385
+ delete span.attributes["app.tenant_name"];
386
+ },
387
+ async forceFlush() {},
388
+ async shutdown() {},
389
+ };
390
+
391
+ await init({ tracing: { spanProcessors: [dropTenant] } });
392
+ ```
393
+
394
+ These processors run in array order after built-in session and device enrichment and before internal wrapper processors and the Interfere batch exporter. Instrumentation and export remain enabled. Complete mutations to `span.attributes` synchronously in `onEnd`; asynchronous callbacks are not awaited, and `setAttribute` cannot change ended spans. Use `privacy.urls` for query values and fragments.
395
+
396
+ The SDK owns processor flushing and shutdown. Give each SDK initialization fresh processor instances, and do not share them with another provider. `tracing: false` skips browser tracing setup. These options do not configure server tracing or `withInterfere()`.
397
+
294
398
  ## What's Included
295
399
 
296
400
  - **Error tracking** — automatic capture of server and client errors with rich stack traces
package/dist/config.d.mts CHANGED
@@ -27,28 +27,13 @@ export interface NextConfigWithInterfere extends NextConfig {
27
27
  */
28
28
  export interface NextPluginConfig {
29
29
  readonly sourceMaps?: {
30
- /**
31
- * Delete source maps from the build output after they've been uploaded
32
- * to Interfere.
33
- *
34
- * Defaults to `true`: maps are uploaded so Interfere can symbolicate
35
- * stack traces, then removed from the build so your original source is
36
- * never shipped to production.
37
- *
38
- * Set to `false` to keep the maps on disk after upload. Do this when another
39
- * tool shares the same Next.js build output and runs its own source-map
40
- * upload from `.next` — Interfere's default purge would delete the maps
41
- * that tool needs.
42
- *
43
- * @default true
44
- * @example
45
- * ```ts
46
- * // Keep maps so another source-map uploader can consume them too:
47
- * export default withInterfere({ sourceMaps: { purge: false } }, nextConfig);
48
- * ```
49
- */
30
+ /** Upload source maps to Interfere. Defaults to true. Release registration and telemetry stay enabled when false. */
31
+ readonly upload?: boolean;
32
+ /** Delete discovered source maps and their references after the build hook, including upload failures or disabled uploads. Defaults to true. */
50
33
  readonly purge?: boolean;
51
34
  };
35
+ /** Turns Interfere off for this build: no release, no source-map upload, and the browser and server SDKs send no events. */
36
+ readonly disable?: boolean;
52
37
  }
53
38
  export interface ResolvedBuildConfig {
54
39
  readonly apiKey: string | null;
@@ -72,12 +57,13 @@ export interface ResolvedBuildConfig {
72
57
  readonly publicKey: string | null;
73
58
  /**
74
59
  * When `true` (default), the build pipeline deletes source maps from the
75
- * build output after uploading them, so they never ship to production.
60
+ * build output whether uploading succeeds, fails, or is disabled.
76
61
  * Resolved from `NextPluginConfig.sourceMaps.purge`; set it `false` to
77
62
  * leave maps on disk for another uploader that shares the same `.next`
78
63
  * output.
79
64
  */
80
65
  readonly purgeSourceMaps: boolean;
66
+ readonly uploadSourceMaps: boolean;
81
67
  /**
82
68
  * Deterministically derived from the leading 16 hex of the commit SHA, so
83
69
  * the build step and the collector arrive at the same
package/dist/config.mjs CHANGED
@@ -1 +1 @@
1
- import{configureBuild}from"./internal/build/configure-build.mjs";import{runGitCommand}from"./internal/build/release/git.mjs";import{readInterfereEnv}from"./internal/env.mjs";import{FatalError,log}from"./internal/logger.mjs";import{resolveReleaseSlug}from"./internal/release-slug.mjs";import{readNextMajorVersion,warnIfServerInstrumentationMissing}from"./internal/setup-warnings.mjs";import{API_URL}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{LOCAL_DEV_RELEASE_SLUG}from"@interfere/types/releases/slug";import{classifyReleasePipelineError,diagnoseRelease,isInterfereApiKey}from"@interfere/types/sdk/build-diagnostics";import{resolveDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{API_URL_GLOBAL,BUILD_MARKER_GLOBAL,ENVIRONMENT_GLOBAL,FORCE_ENABLE_GLOBAL,PUBLIC_KEY_GLOBAL,RELEASE_SLUG_GLOBAL}from"@interfere/types/sdk/injected-globals";import{PHASE_PRODUCTION_BUILD}from"next/constants.js";const CLIENT_TRACE_METADATA_KEYS=[`traceparent`,`tracestate`,`baggage`],DIAGNOSTIC_CONTEXT={publicKeyEnvVar:`INTERFERE_PUBLIC_KEY`};function reportReleaseDiagnostic(code,input){let diagnostic=diagnoseRelease(code,DIAGNOSTIC_CONTEXT,input),lines=[...diagnostic.details];if(diagnostic.severity===`fatal`){log.fatal(diagnostic.title,lines);return}log.warn(diagnostic.title,lines)}function isThenable(value){return typeof value==`object`&&!!value&&`then`in value&&typeof value.then==`function`}function withInterfere(...args){let[settings,nextConfig]=args.length===2?[args[0],args[1]]:[{},args[0]??{}];return(phase,context)=>{let resolved=typeof nextConfig==`function`?nextConfig(phase,context??{defaultConfig:{}}):nextConfig;return isThenable(resolved)?Promise.resolve(resolved).then(config=>buildInterfereConfig(phase,settings,config)):buildInterfereConfig(phase,settings,resolved)}}function interfereInjectedValues(config,apiHost,forceEnable){let releaseSlug=forceEnable?LOCAL_DEV_RELEASE_SLUG:config.releaseSlug;return omitUndefined({[RELEASE_SLUG_GLOBAL]:releaseSlug,[BUILD_MARKER_GLOBAL]:!0,[PUBLIC_KEY_GLOBAL]:config.publicKey||void 0,[API_URL_GLOBAL]:config.publicKey&&apiHost?apiHost:void 0,[ENVIRONMENT_GLOBAL]:config.environment||void 0,[FORCE_ENABLE_GLOBAL]:forceEnable?!0:void 0})}function interfereCompilerConfig(compiler,config){let existingHook=compiler?.runAfterProductionCompile;return omitUndefined({...compiler,async runAfterProductionCompile(context){existingHook&&await existingHook(context);try{await runBuildReleasePipeline(context,config)}catch(error){if(error instanceof FatalError)throw error;let message=error instanceof Error?error.message:String(error);log.warn(`Skipping release pipeline`,[`Interfere encountered an unexpected error.`,`Build will continue, but this release's data will be ingested, but ignored as finding when generating insights.`,`Error: ${message}`])}}})}function resolveBrowserSourceMaps(config,productionBrowserSourceMaps){return config.apiKey!==null&&productionBrowserSourceMaps===!1&&log.warn(`Forcing productionBrowserSourceMaps`,[`withInterfere() requires production source maps to symbolicate errors.`,"Your `productionBrowserSourceMaps: false` was overridden to `true`."]),config.apiKey!==null||productionBrowserSourceMaps}function buildInterfereConfig(phase,settings,nextConfig){let{interfere,env:userEnv,webpack,turbopack,compiler,productionBrowserSourceMaps,...rest}=nextConfig;if(interfere&&`buildId`in interfere&&interfere.buildId!==void 0)return log.fatal(`Removed: interfere.buildId`,["`interfere.buildId` was removed in SDK 10. It only worked at build time, so server-side spans drifted from the bundle's slug.","Override the commit SHA via the `INTERFERE_SOURCE_ID` env var on both build and runtime envs instead."]);let projectDir=process.cwd(),config=resolveBuildConfig(settings,phase),apiHost=resolveBrowserApiHost(interfere?.apiHost,config.apiUrl),forceEnable=!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE,nextMajor=readNextMajorVersion(projectDir),build=configureBuild({existingTurbopack:turbopack,existingWebpack:webpack,nextMajor,projectDir,remoteConfigFetch:resolveRemoteConfigFetch(config,phase),values:interfereInjectedValues(config,apiHost,forceEnable)});if(config.publicKey!==null&&!build.clientInstrumentationApplies)return log.fatal(`Missing instrumentation client`,[`INTERFERE_PUBLIC_KEY is set but no instrumentation-client file was found.`,`Create an instrumentation-client.ts file in your project root or src/ directory.`]);config.publicKey!==null&&warnIfServerInstrumentationMissing(projectDir);let experimental=interfereExperimentalConfig({existing:rest.experimental,instrumentationHook:nextMajor===null||nextMajor<=14,traceMetadata:config.publicKey!==null||forceEnable});return omitUndefined({...rest,env:userEnv,compiler:interfereCompilerConfig(compiler,config),productionBrowserSourceMaps:resolveBrowserSourceMaps(config,productionBrowserSourceMaps),turbopack:build.turbopack,webpack:build.webpack,experimental})}function interfereExperimentalConfig({existing,instrumentationHook,traceMetadata}){let merged={...existing};return instrumentationHook&&(merged.instrumentationHook=!0),traceMetadata&&(merged.clientTraceMetadata=[...new Set([...existing?.clientTraceMetadata??[],...CLIENT_TRACE_METADATA_KEYS])]),Object.keys(merged).length>0?merged:void 0}function resolveBrowserApiHost(apiHost,apiUrl){let host=apiHost??apiUrl;if(host!==API_URL)return normalizeHost(host)}function normalizeHost(host){try{return new URL(host).origin}catch{return log.fatal(`Invalid apiHost`,[`interfere.apiHost must be a full URL, e.g. "https://in.yourapp.com".`,`Received: ${host}`])}}function resolveBuildConfig(settings,phase){let{apiKey,apiUrl,publicKey}=readInterfereEnv(),{commitSha,slug}=resolveReleaseSlug(runGitCommand),environment=phase===PHASE_PRODUCTION_BUILD?resolveDeployEnvironment(process.env):null;return{apiKey:apiKey??null,apiUrl,buildId:commitSha,environment,publicKey:publicKey??null,purgeSourceMaps:settings.sourceMaps?.purge??!0,releaseSlug:slug}}function resolveRemoteConfigFetch(config,phase){if(phase===PHASE_PRODUCTION_BUILD&&config.publicKey)return{apiUrl:config.apiUrl,cacheScope:crypto.randomUUID(),publicKey:config.publicKey}}async function runBuildReleasePipeline(context,config){let{apiKey,buildId,releaseSlug}=config;if(!apiKey)return log.fatal(`Secret key not set`,[`withInterfere() requires a secret key to publish release metadata during production builds.`,`Set the INTERFERE_API_KEY environment variable, or remove withInterfere() from your Next.js config.`,`See: https://interfere.com/docs/sdk/next-js`]);if(!isInterfereApiKey(apiKey)){reportReleaseDiagnostic(`API_KEY_WRONG_PREFIX`,{apiKey});return}if(!(buildId&&releaseSlug))return log.fatal(`Commit SHA missing`,[`Could not resolve a commit SHA from environment variables or git.`,"Set `INTERFERE_SOURCE_ID`, ensure your CI exposes its git SHA env var (e.g. `VERCEL_GIT_COMMIT_SHA`, `GITHUB_SHA`), or run inside a git checkout."]);let{runBuildPipeline}=await import(`./internal/build/pipeline.mjs`);try{let result=await runBuildPipeline(context,{...config,apiKey,buildId,releaseSlug});if(!result.accepted){log.warn(`Release remains unready`,[`No source maps found. Enable source-map generation and upload them during the build.`]);return}log.info(`Completed`,[`https://interfere.com/~/${result.config.org.slug}/surfaces/${result.config.surface.slug}`,`Release: ${result.release.destination.slug}`,`Build: ${result.buildId}`,`Artifacts: ${result.fileCount} source maps`]),result.cleanupFailures.length>0&&log.warn(`Source-map cleanup left files on disk`,result.cleanupFailures.map(failure=>`${failure.absolute}: ${failure.message}`))}catch(error){let message=error instanceof Error?error.message:String(error);reportReleaseDiagnostic(classifyReleasePipelineError(error),{errorMessage:message})}}export{withInterfere};
1
+ import{configureBuild}from"./internal/build/configure-build.mjs";import{runGitCommand}from"./internal/build/release/git.mjs";import{readInterfereEnv}from"./internal/env.mjs";import{FatalError,log}from"./internal/logger.mjs";import{resolveReleaseSlug}from"./internal/release-slug.mjs";import{readNextMajorVersion,warnIfServerInstrumentationMissing}from"./internal/setup-warnings.mjs";import{API_URL}from"@interfere/constants/api";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{LOCAL_DEV_RELEASE_SLUG}from"@interfere/types/releases/slug";import{checkReleaseConfig,diagnoseRelease}from"@interfere/types/sdk/build-diagnostics";import{resolveDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{API_URL_GLOBAL,BUILD_MARKER_GLOBAL,DISABLED_GLOBAL,ENVIRONMENT_GLOBAL,FORCE_ENABLE_GLOBAL,PUBLIC_KEY_GLOBAL,RELEASE_SLUG_GLOBAL}from"@interfere/types/sdk/injected-globals";import{PHASE_PRODUCTION_BUILD}from"next/constants.js";const CLIENT_TRACE_METADATA_KEYS=[`traceparent`,`tracestate`,`baggage`],DIAGNOSTIC_CONTEXT={publicKeyEnvVar:`INTERFERE_PUBLIC_KEY`,docsUrl:`https://interfere.com/docs/sdk/next-js`};function isThenable(value){return typeof value==`object`&&!!value&&`then`in value&&typeof value.then==`function`}function withInterfere(...args){let[settings,nextConfig]=args.length===2?[args[0],args[1]]:[{},args[0]??{}];return(phase,context)=>{let resolved=typeof nextConfig==`function`?nextConfig(phase,context??{defaultConfig:{}}):nextConfig;return isThenable(resolved)?Promise.resolve(resolved).then(config=>buildInterfereConfig(phase,settings,config)):buildInterfereConfig(phase,settings,resolved)}}function interfereInjectedValues(config,apiHost,forceEnable){let releaseSlug=forceEnable?LOCAL_DEV_RELEASE_SLUG:config.releaseSlug;return omitUndefined({[RELEASE_SLUG_GLOBAL]:releaseSlug,[BUILD_MARKER_GLOBAL]:!0,[PUBLIC_KEY_GLOBAL]:config.publicKey||void 0,[API_URL_GLOBAL]:config.publicKey&&apiHost?apiHost:void 0,[ENVIRONMENT_GLOBAL]:config.environment||void 0,[FORCE_ENABLE_GLOBAL]:forceEnable?!0:void 0})}function interfereCompilerConfig(compiler,config){let existingHook=compiler?.runAfterProductionCompile;return omitUndefined({...compiler,async runAfterProductionCompile(context){existingHook&&await existingHook(context),config.publicKey===null&&log.warn(`Public key not set`,[`INTERFERE_PUBLIC_KEY is not set, so this build sends no events.`,`Set it on builds that you deploy.`]);try{await runBuildReleasePipeline(context,config)}catch(error){if(error instanceof FatalError)throw error;let message=error instanceof Error?error.message:String(error);log.warn(`Skipping release pipeline`,[`Interfere encountered an unexpected error.`,`Build will continue, but this release's data will be ingested, but ignored as finding when generating insights.`,`Error: ${message}`])}}})}function resolveBrowserSourceMaps(config,productionBrowserSourceMaps){let releasing=config.apiKey!==null&&config.uploadSourceMaps;return releasing&&productionBrowserSourceMaps===!1&&log.warn(`Forcing productionBrowserSourceMaps`,[`withInterfere() requires production source maps to symbolicate errors.`,"Your `productionBrowserSourceMaps: false` was overridden to `true`."]),releasing?!0:productionBrowserSourceMaps}function disabledInterfereConfig(nextConfig){let{interfere:_interfere,webpack,turbopack,...rest}=nextConfig,projectDir=process.cwd(),build=configureBuild({existingTurbopack:turbopack,existingWebpack:webpack,nextMajor:readNextMajorVersion(projectDir),projectDir,remoteConfigFetch:void 0,values:{[BUILD_MARKER_GLOBAL]:!0,[DISABLED_GLOBAL]:!0,[RELEASE_SLUG_GLOBAL]:null}});return omitUndefined({...rest,turbopack:build.turbopack,webpack:build.webpack})}function buildInterfereConfig(phase,settings,nextConfig){if(settings.disable)return disabledInterfereConfig(nextConfig);let{interfere,env:userEnv,webpack,turbopack,compiler,productionBrowserSourceMaps,...rest}=nextConfig;if(interfere&&`buildId`in interfere&&interfere.buildId!==void 0)return log.fatal(`Removed: interfere.buildId`,["`interfere.buildId` was removed in SDK 10. It only worked at build time, so server-side spans drifted from the bundle's slug.","Override the commit SHA via the `INTERFERE_SOURCE_ID` env var on both build and runtime envs instead."]);let projectDir=process.cwd(),config=resolveBuildConfig(settings,phase),apiHost=resolveBrowserApiHost(interfere?.apiHost,config.apiUrl),forceEnable=!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE,nextMajor=readNextMajorVersion(projectDir),build=configureBuild({existingTurbopack:turbopack,existingWebpack:webpack,nextMajor,projectDir,remoteConfigFetch:resolveRemoteConfigFetch(config,phase),values:interfereInjectedValues(config,apiHost,forceEnable)});if(config.publicKey!==null&&!build.clientInstrumentationApplies)return log.fatal(`Missing instrumentation client`,[`INTERFERE_PUBLIC_KEY is set but no instrumentation-client file was found.`,`Create an instrumentation-client.ts file in your project root or src/ directory.`]);config.publicKey!==null&&warnIfServerInstrumentationMissing(projectDir);let experimental=interfereExperimentalConfig({existing:rest.experimental,instrumentationHook:nextMajor===null||nextMajor<=14,traceMetadata:config.publicKey!==null||forceEnable});return omitUndefined({...rest,env:userEnv,compiler:interfereCompilerConfig(compiler,config),productionBrowserSourceMaps:resolveBrowserSourceMaps(config,productionBrowserSourceMaps),turbopack:build.turbopack,webpack:build.webpack,experimental})}function interfereExperimentalConfig({existing,instrumentationHook,traceMetadata}){let merged={...existing};return instrumentationHook&&(merged.instrumentationHook=!0),traceMetadata&&(merged.clientTraceMetadata=[...new Set([...existing?.clientTraceMetadata??[],...CLIENT_TRACE_METADATA_KEYS])]),Object.keys(merged).length>0?merged:void 0}function resolveBrowserApiHost(apiHost,apiUrl){let host=apiHost??apiUrl;if(host!==API_URL)return normalizeHost(host)}function normalizeHost(host){try{return new URL(host).origin}catch{return log.fatal(`Invalid apiHost`,[`interfere.apiHost must be a full URL, e.g. "https://in.yourapp.com".`,`Received: ${host}`])}}function resolveBuildConfig(settings,phase){let{apiKey,apiUrl,publicKey}=readInterfereEnv(),{commitSha,slug}=resolveReleaseSlug(runGitCommand),environment=phase===PHASE_PRODUCTION_BUILD?resolveDeployEnvironment(process.env):null;return{apiKey:apiKey??null,apiUrl,buildId:commitSha,environment,publicKey:publicKey??null,purgeSourceMaps:settings.sourceMaps?.purge??!0,uploadSourceMaps:settings.sourceMaps?.upload??!0,releaseSlug:slug}}function resolveRemoteConfigFetch(config,phase){if(phase===PHASE_PRODUCTION_BUILD&&config.publicKey)return{apiUrl:config.apiUrl,cacheScope:crypto.randomUUID(),publicKey:config.publicKey}}async function runBuildReleasePipeline(context,config){let release=checkReleaseConfig(DIAGNOSTIC_CONTEXT,{apiKey:config.apiKey,commitSha:config.buildId,releaseSlug:config.releaseSlug});if(release.status===`skip`){log.warn(release.title,[...release.details]);return}if(release.status===`fail`)return log.fatal(release.title,[...release.details]);let{runBuildPipeline}=await import(`./internal/build/pipeline.mjs`);try{let result=await runBuildPipeline(context,{...config,apiKey:release.apiKey,buildId:release.commitSha,releaseSlug:release.releaseSlug});if(!result.accepted){result.reason===`no_source_maps`&&log.warn(`Release remains unready`,[`No source maps found. Enable source-map generation and upload them during the build.`]);return}log.info(`Completed`,[`https://interfere.com/~/${result.config.org.slug}/surfaces/${result.config.surface.slug}`,`Release: ${result.release.destination.slug}`,`Build: ${result.buildId}`,config.uploadSourceMaps?`Artifacts: ${result.fileCount} source maps`:`Source-map uploads disabled`])}catch(error){let diagnostic=diagnoseRelease(DIAGNOSTIC_CONTEXT,error);log[diagnostic.severity](diagnostic.title,[...diagnostic.details])}}export{withInterfere};
@@ -1 +1 @@
1
- import{buildIntegrationRan,hasEnvCredentials,warnBuildIntegrationDegraded,warnBuildIntegrationMissing}from"./internal/build-marker-guard.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{createWrapperSingleton}from"@interfere/react/internal/wrapper-singleton";import{capture,span}from"@interfere/react/api";const wrapper=createWrapperSingleton({initEntryName:`instrumentation-client`,producerVersion:PRODUCER_VERSION}),{close,consent,getKernel,getKernelOrNull,identity,subscribeToKernel}=wrapper;async function init(opts){return buildIntegrationRan()?wrapper.init(opts):initAfterHoistedImports(opts)}async function initAfterHoistedImports(opts){if(await Promise.resolve(),!buildIntegrationRan()){let{promise,resolve}=Promise.withResolvers();setTimeout(resolve,0),await promise}return buildIntegrationRan()?wrapper.init(opts):hasEnvCredentials()?(warnBuildIntegrationDegraded(),wrapper.init(opts)):(warnBuildIntegrationMissing(),null)}export{capture,close,consent,getKernel,getKernelOrNull,identity,init,span,subscribeToKernel};
1
+ import{buildIntegrationRan,hasEnvCredentials,interfereDisabled,warnBuildIntegrationDegraded,warnBuildIntegrationMissing}from"./internal/build-marker-guard.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{createWrapperSingleton}from"@interfere/react/internal/wrapper-singleton";import{capture,span}from"@interfere/react/api";const wrapper=createWrapperSingleton({initEntryName:`instrumentation-client`,producerVersion:PRODUCER_VERSION}),{close,consent,getKernel,getKernelOrNull,identity,subscribeToKernel}=wrapper;async function init(opts){return buildIntegrationRan()?initKernel(opts):initAfterHoistedImports(opts)}async function initKernel(opts){return interfereDisabled()?null:wrapper.init(opts)}async function initAfterHoistedImports(opts){if(await Promise.resolve(),!buildIntegrationRan()){let{promise,resolve}=Promise.withResolvers();setTimeout(resolve,0),await promise}return buildIntegrationRan()?initKernel(opts):hasEnvCredentials()?(warnBuildIntegrationDegraded(),wrapper.init(opts)):(warnBuildIntegrationMissing(),null)}export{capture,close,consent,getKernel,getKernelOrNull,identity,init,span,subscribeToKernel};
@@ -1 +1 @@
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
+ 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{createUrlRedactionSpanProcessor,resolveUrlRedaction}from"@interfere/types/sdk/url-redaction";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:[createUrlRedactionSpanProcessor(resolveUrlRedaction(opts.privacy?.urls)),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{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
+ 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{createUrlRedactionSpanProcessor,resolveUrlRedaction}from"@interfere/types/sdk/url-redaction";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=[createUrlRedactionSpanProcessor(resolveUrlRedaction(opts.privacy?.urls)),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};
@@ -3,6 +3,7 @@ import { NextConfig } from "next";
3
3
  interface InterfereInjectedValues {
4
4
  readonly __INTERFERE_API_URL__?: string;
5
5
  readonly __INTERFERE_BUILD__?: boolean;
6
+ readonly __INTERFERE_DISABLED__?: boolean;
6
7
  readonly __INTERFERE_ENVIRONMENT__?: string;
7
8
  readonly __INTERFERE_FORCE_ENABLE__?: boolean;
8
9
  readonly __INTERFERE_PUBLIC_KEY__?: string;
@@ -7,6 +7,15 @@ export declare function runBuildPipeline(context: ProductionCompileContext, meta
7
7
  buildId: string;
8
8
  releaseSlug: ReleaseSlug;
9
9
  }): Promise<{
10
+ accepted: false;
11
+ fileCount: number;
12
+ reason: "missing_destination_provider" | "missing_source_provider";
13
+ buildId?: never;
14
+ cleanupFailures?: never;
15
+ config?: never;
16
+ release?: never;
17
+ timing?: never;
18
+ } | {
10
19
  accepted: false;
11
20
  fileCount: number;
12
21
  reason: "no_source_maps";
@@ -1 +1 @@
1
- import{readNextMajorVersion}from"../setup-warnings.mjs";import{detectActiveBundler}from"./detect-bundler.mjs";import{resolveReleaseRequest}from"./release/index.mjs";import{discoverSourceMaps}from"./source-maps/discover.mjs";import{assertClientChunksAreMapped}from"./source-maps/guard.mjs";import{EMPTY_TELEMETRY}from"./telemetry.mjs";import{classifyReleasePipelineError}from"@interfere/types/sdk/build-diagnostics";import{InterfereClient}from"@interfere/javascript";import{cleanupSourceMaps}from"@interfere/build/cleanup-source-maps";import{uploadSourceMapChunks}from"@interfere/build/upload-source-map-chunks";import{completeSourceMapsResponse}from"@interfere/types/data/source-maps";import{releaseBuildResultSchema}from"@interfere/types/releases/build-telemetry";import{createReleaseResponse}from"@interfere/types/releases/definition";import{extractRichness}from"@interfere/types/sdk/source-map-richness";async function runBuildPipeline(context,metadata){let client=new InterfereClient({baseUrl:metadata.apiUrl,apiKey:{token:metadata.apiKey},maxRetries:4,timeoutInSeconds:10}),start=performance.now(),timing={...EMPTY_TELEMETRY},{releaseSlug}=metadata,report=async result=>{timing.total=Math.round(performance.now()-start),await client.releases.reportTelemetry({...timing,result,releaseSlug},{abortSignal:AbortSignal.timeout(1e4)}).catch(()=>void 0)};try{let bundler=detectActiveBundler(process.env,process.argv,readNextMajorVersion(context.projectDir)),[discovered,config]=await Promise.all([discoverSourceMaps({bundler,distDir:context.distDir,projectDir:context.projectDir}),client.releases.getReleasesConfig({abortSignal:AbortSignal.timeout(6e4)})]),{files,sourceFileCount}=discovered;if(timing.discover=Math.round(performance.now()-start),timing.fileCount=files.length,files.length===0)return await report(`no_source_maps`),{accepted:!1,fileCount:0,reason:`no_source_maps`};await assertClientChunksAreMapped({discovered:files,distDir:context.distDir,projectDir:context.projectDir});let registering=performance.now(),release=createReleaseResponse.parse(await client.releases.createRelease(resolveReleaseRequest(metadata.buildId,metadata.environment,metadata.releaseSlug,config),{abortSignal:AbortSignal.timeout(6e4)})),registered=performance.now();releaseSlug=release.destination.slug,timing.createRelease=Math.round(registered-registering);let signFiles=files.map(file=>({path:file.path,sizeBytes:new TextEncoder().encode(file.content).byteLength,...extractRichness(file.content)})),environment=metadata.environment??void 0,signed=await client.releases.signSourceMapUploads({releaseSlug,environment,files:signFiles},{abortSignal:AbortSignal.timeout(6e4)});timing.totalBytes=await uploadSourceMapChunks({client,environment,releaseSlug,files,signed});let accepted=completeSourceMapsResponse.safeParse(await client.releases.finalizeSourceMaps({releaseSlug,environment,bundler,sourceFileCount,files:files.map(({path,hash,debugId,chunkUrl})=>({path,hash,debugId,chunkUrl}))},{abortSignal:AbortSignal.timeout(Math.max(0,signed.expiresAt*1e3-Date.now()))}));if(!accepted.success||!accepted.data.ok||accepted.data.fileCount!==files.length)throw Error(`Source-map server did not accept the complete batch`);let cleanupStart=performance.now();timing.upload=Math.round(cleanupStart-registered);let cleanupFailures=metadata.purgeSourceMaps?await cleanupSourceMaps(files):[];return timing.cleanup=Math.round(performance.now()-cleanupStart),await report(`ok`),{accepted:!0,buildId:release.build.hash,cleanupFailures,config,fileCount:files.length,release,timing}}catch(error){let parsed=releaseBuildResultSchema.safeParse(classifyReleasePipelineError(error).toLowerCase());throw await report(parsed.success?parsed.data:`pipeline_failed`),error}}export{runBuildPipeline};
1
+ import{log}from"../logger.mjs";import{readNextMajorVersion}from"../setup-warnings.mjs";import{detectActiveBundler}from"./detect-bundler.mjs";import{resolveReleaseRequest}from"./release/index.mjs";import{discoverSourceMaps}from"./source-maps/discover.mjs";import{reportUnmappedClientChunks}from"./source-maps/guard.mjs";import{EMPTY_TELEMETRY}from"./telemetry.mjs";import{classifyReleasePipelineError}from"@interfere/types/sdk/build-diagnostics";import{InterfereClient}from"@interfere/javascript";import{sourceMapUploadFetch}from"@interfere/build/source-map-upload-fetch";import{cleanupSourceMaps}from"@interfere/build/cleanup-source-maps";import{uploadSourceMapChunks}from"@interfere/build/upload-source-map-chunks";import{completeSourceMapsResponse}from"@interfere/types/data/source-maps";import{releaseBuildResultSchema}from"@interfere/types/releases/build-telemetry";import{createReleaseResponse}from"@interfere/types/releases/definition";import{extractRichness}from"@interfere/types/sdk/source-map-richness";async function runBuildPipeline(context,metadata){let client=new InterfereClient({fetch:sourceMapUploadFetch,baseUrl:metadata.apiUrl,apiKey:{token:metadata.apiKey},maxRetries:4,timeoutInSeconds:10}),start=performance.now(),timing={...EMPTY_TELEMETRY},{releaseSlug}=metadata,files=[],cleanupFailures=[],reportResult=`pipeline_failed`;try{let bundler=detectActiveBundler(process.env,process.argv,readNextMajorVersion(context.projectDir)),discovered=metadata.uploadSourceMaps||metadata.purgeSourceMaps?await discoverSourceMaps({bundler,...context}):{files:[],missingSourceMapCount:0,sourceFileCount:0};({files}=discovered);let{sourceFileCount}=discovered;metadata.uploadSourceMaps&&discovered.missingSourceMapCount>0&&log.warn(`Some source maps are missing`,[`${discovered.missingSourceMapCount} referenced source map(s) not found. Errors in those chunks will not resolve to source.`]);let config=await client.releases.getReleasesConfig({abortSignal:AbortSignal.timeout(6e4)});timing.discover=Math.round(performance.now()-start),timing.fileCount=files.length;let request=resolveReleaseRequest(metadata.buildId,metadata.environment,metadata.releaseSlug,config);if(!request.ok)return reportResult=null,{accepted:!1,fileCount:0,reason:request.reason};if(metadata.uploadSourceMaps&&files.length===0)return reportResult=`no_source_maps`,{accepted:!1,fileCount:0,reason:`no_source_maps`};metadata.uploadSourceMaps&&await reportUnmappedClientChunks({discovered:files,...context});let registering=performance.now(),release=createReleaseResponse.parse(await client.releases.createRelease(request.request,{abortSignal:AbortSignal.timeout(6e4)})),registered=performance.now();if(releaseSlug=release.destination.slug,timing.createRelease=Math.round(registered-registering),metadata.uploadSourceMaps){let signFiles=files.map(file=>({path:file.path,sizeBytes:new TextEncoder().encode(file.content).byteLength,...extractRichness(file.content)})),environment=metadata.environment??void 0,signed=await client.releases.signSourceMapUploads({releaseSlug,environment,files:signFiles},{abortSignal:AbortSignal.timeout(6e4)});timing.totalBytes=await uploadSourceMapChunks({compression:`gzip`,client,environment,releaseSlug,files,signed});let accepted=completeSourceMapsResponse.safeParse(await client.releases.finalizeSourceMaps({releaseSlug,environment,bundler,sourceFileCount,files:files.map(({path,hash,debugId,chunkUrl})=>({path,hash,debugId,chunkUrl}))},{abortSignal:AbortSignal.timeout(Math.max(0,signed.expiresAt*1e3-Date.now()))}));if(!accepted.success||!accepted.data.ok||accepted.data.fileCount!==files.length)throw Error(`Source-map server did not accept the complete batch`)}return timing.upload=metadata.uploadSourceMaps?Math.round(performance.now()-registered):0,reportResult=`ok`,{accepted:!0,buildId:release.build.hash,cleanupFailures,config,fileCount:metadata.uploadSourceMaps?files.length:0,release,timing}}catch(error){throw reportResult=releaseBuildResultSchema.safeParse(classifyReleasePipelineError(error).toLowerCase()).data??`pipeline_failed`,error}finally{let cleanupStart=performance.now();if(metadata.purgeSourceMaps)try{cleanupFailures.push(...await cleanupSourceMaps(files))}catch(error){cleanupFailures.push({absolute:context.distDir,code:void 0,message:error instanceof Error?error.message:String(error)})}timing.cleanup=Math.round(performance.now()-cleanupStart),cleanupFailures.length>0&&log.warn(`Source-map cleanup left files on disk`,cleanupFailures.map(failure=>`${failure.absolute}: ${failure.message}`)),timing.total=Math.round(performance.now()-start),reportResult!==null&&await client.releases.reportTelemetry({...timing,result:reportResult,releaseSlug},{abortSignal:AbortSignal.timeout(1e4)}).catch(()=>void 0)}}export{runBuildPipeline};
@@ -2,52 +2,66 @@ import { ReleaseSlug } from "@interfere/types/releases/slug";
2
2
  import { resolveReleaseRequest as resolveReleaseRequest$1 } from "@interfere/build/release-request";
3
3
  //#region src/internal/build/release/index.d.ts
4
4
  export declare function resolveReleaseRequest(buildId: string, environment: string | null, releaseSlug: ReleaseSlug, config: Parameters<typeof resolveReleaseRequest$1>[0]["config"]): {
5
- buildId: string;
6
- destination: {
7
- deploymentId: string | null;
8
- deploymentUrl: string | null;
9
- destinationReleaseId: string | null;
10
- environment: string | null;
11
- environmentName: string | null;
12
- environmentTarget: string | null;
13
- provider: "vercel";
14
- } | {
15
- accountId: string | null;
16
- deploymentUrl: string | null;
17
- destinationReleaseId: string | null;
18
- environment: string | null;
19
- environmentName: string | null;
20
- environmentTarget: string | null;
21
- provider: "cloudflare";
22
- scriptName: string | null;
23
- versionId: string | null;
24
- } | {
25
- deploymentUrl: string | null;
26
- destinationReleaseId: string | null;
27
- environment: string | null;
28
- provider: "cli";
29
- } | null;
30
- producerVersion?: string | undefined;
31
- slug?: string | undefined;
32
- source: {
33
- branch: string;
34
- commitMessage: string;
35
- commitSha: string | null;
36
- provider: "github";
37
- runId?: string | null | undefined;
38
- runUrl?: string | null | undefined;
39
- runnerName?: string | null | undefined;
40
- } | {
41
- commitMessage: string | null;
42
- commitRef: string | null;
43
- commitSha: string | null;
44
- commitUrl: string | null;
45
- deploymentUrl?: string | null | undefined;
46
- environment?: string | null | undefined;
47
- provider: "cli";
48
- runId: string | null;
49
- runUrl: string | null;
50
- runnerName: string | null;
5
+ ok: false;
6
+ reason: "missing_source_provider";
7
+ request?: never;
8
+ } | {
9
+ ok: false;
10
+ reason: "missing_destination_provider";
11
+ request?: never;
12
+ } | {
13
+ reason?: never;
14
+ ok: true;
15
+ request: {
16
+ buildId: string;
17
+ destination: {
18
+ deploymentId: string | null;
19
+ deploymentUrl: string | null;
20
+ destinationReleaseId: string | null;
21
+ environment: string | null;
22
+ environmentName: string | null;
23
+ environmentTarget: string | null;
24
+ provider: "vercel";
25
+ } | {
26
+ accountId: string | null;
27
+ deploymentUrl: string | null;
28
+ destinationReleaseId: string | null;
29
+ environment: string | null;
30
+ environmentName: string | null;
31
+ environmentTarget: string | null;
32
+ provider: "cloudflare";
33
+ deploymentId?: string | null | undefined;
34
+ trafficPercentage?: number | null | undefined;
35
+ scriptName: string | null;
36
+ versionId: string | null;
37
+ } | {
38
+ deploymentUrl: string | null;
39
+ destinationReleaseId: string | null;
40
+ environment: string | null;
41
+ provider: "cli";
42
+ } | null;
43
+ producerVersion?: string | undefined;
44
+ slug?: string | undefined;
45
+ source: {
46
+ branch: string;
47
+ commitMessage: string;
48
+ commitSha: string | null;
49
+ provider: "github";
50
+ runId?: string | null | undefined;
51
+ runUrl?: string | null | undefined;
52
+ runnerName?: string | null | undefined;
53
+ } | {
54
+ commitMessage: string | null;
55
+ commitRef: string | null;
56
+ commitSha: string | null;
57
+ commitUrl: string | null;
58
+ deploymentUrl?: string | null | undefined;
59
+ environment?: string | null | undefined;
60
+ provider: "cli";
61
+ runId: string | null;
62
+ runUrl: string | null;
63
+ runnerName: string | null;
64
+ };
51
65
  };
52
66
  };
53
67
  //#endregion
@@ -1 +1 @@
1
- import{log}from"../../logger.mjs";import{PRODUCER_VERSION}from"../../version.mjs";import{resolveReleaseRequest as resolveReleaseRequest$1}from"@interfere/build/release-request";function resolveReleaseRequest(buildId,environment,releaseSlug,config){let result=resolveReleaseRequest$1({buildId,config,environment,producerVersion:PRODUCER_VERSION,releaseSlug});if(result.ok)return result.request;let sourceMissing=result.reason===`missing_source_provider`;return log.fatal(sourceMissing?`Missing version control provider`:`Missing deployment target`,[sourceMissing?`This surface does not have a version control provider configured.`:`This surface does not have a deployment target integration configured.`,`Configure one at https://interfere.com/~/${config.org.slug}/surfaces/${config.surface.slug}`])}export{resolveReleaseRequest};
1
+ import{log}from"../../logger.mjs";import{PRODUCER_VERSION}from"../../version.mjs";import{resolveReleaseRequest as resolveReleaseRequest$1}from"@interfere/build/release-request";function resolveReleaseRequest(buildId,environment,releaseSlug,config){let result=resolveReleaseRequest$1({buildId,config,environment,producerVersion:PRODUCER_VERSION,releaseSlug});if(result.ok)return result;let sourceMissing=result.reason===`missing_source_provider`;return log.warn(sourceMissing?`Release skipped: missing version control provider`:`Release skipped: missing deployment target`,[sourceMissing?`This surface does not have a version control provider configured.`:`This surface does not have a deployment target integration configured.`,`Configure one at https://interfere.com/~/${config.org.slug}/surfaces/${config.surface.slug}`]),result}export{resolveReleaseRequest};
@@ -25,6 +25,7 @@ export declare function discoverTurbopack(opts: {
25
25
  distDir: string;
26
26
  }): Promise<{
27
27
  files: SourceMapFile[];
28
+ missingSourceMapCount: number;
28
29
  sourceFileCount: number;
29
30
  }>;
30
31
  //#endregion
@@ -1 +1 @@
1
- import{resolveDistDir,toPublicPath,walkDistTrees}from"./paths.mjs";import{relative}from"node:path";import{createHash}from"node:crypto";import{mapWithConcurrency}from"@interfere/helpers/map-with-concurrency";import{open,readFile,stat}from"node:fs/promises";const DEBUG_ID_RE=/\/\/[#@]\s*debugId=(?<debugId>[0-9a-f-]+)\s*$/mu,DEBUG_ID_TAIL_BYTES=65536;async function readChunkDebugId(jsAbs){let fromTail=await readTailDebugId(jsAbs).catch(()=>null);if(fromTail!==null)return fromTail;let content=await readFile(jsAbs,`utf8`).catch(()=>null);return content===null?null:DEBUG_ID_RE.exec(content)?.groups?.debugId??null}async function readTailDebugId(jsAbs){let{size}=await stat(jsAbs);if(size<=DEBUG_ID_TAIL_BYTES)return null;let handle=await open(jsAbs,`r`);try{let buf=Buffer.allocUnsafe(DEBUG_ID_TAIL_BYTES),{bytesRead}=await handle.read(buf,0,DEBUG_ID_TAIL_BYTES,size-DEBUG_ID_TAIL_BYTES);return DEBUG_ID_RE.exec(buf.subarray(0,bytesRead).toString(`utf8`))?.groups?.debugId??null}finally{await handle.close()}}async function discoverTurbopack(opts){let absDistDir=resolveDistDir(opts.projectDir,opts.distDir),relDistDir=relative(opts.projectDir,absDistDir),[mapPaths,jsPaths]=await Promise.all([walkDistTrees(absDistDir,`.js.map`),walkDistTrees(absDistDir,`.js`)]),chunksByDebugId=new Map;await mapWithConcurrency(jsPaths,16,async jsAbs=>{let debugId=await readChunkDebugId(jsAbs);if(!debugId)return;let chunkUrl=toPublicPath(relative(opts.projectDir,jsAbs),relDistDir),chunk=chunksByDebugId.get(debugId);chunk?chunk.chunkAbsolutes.push(jsAbs):chunksByDebugId.set(debugId,{chunkAbsolutes:[jsAbs],chunkUrl})});let loaded=(await mapWithConcurrency(mapPaths,16,async mapAbs=>{let content=await readFile(mapAbs,`utf8`).catch(()=>null);if(content===null)return null;let debugId;try{let parsed=JSON.parse(content);typeof parsed==`object`&&parsed&&`debugId`in parsed&&typeof parsed.debugId==`string`&&parsed.debugId.length>0&&({debugId}=parsed)}catch{return null}if(!debugId)return null;let chunk=chunksByDebugId.get(debugId);return chunk?{absolute:mapAbs,chunkAbsolutes:chunk.chunkAbsolutes,chunkUrl:chunk.chunkUrl,content,debugId,hash:createHash(`sha256`).update(content).digest(`hex`),path:toPublicPath(relative(opts.projectDir,mapAbs),relDistDir)}:null})).filter(f=>f!==null);return{files:loaded,sourceFileCount:loaded.length}}export{discoverTurbopack};
1
+ import{resolveDistDir,toPublicPath,walkDistTrees}from"./paths.mjs";import{relative}from"node:path";import{createHash}from"node:crypto";import{mapWithConcurrency}from"@interfere/helpers/map-with-concurrency";import{open,readFile,stat}from"node:fs/promises";const DEBUG_ID_RE=/\/\/[#@]\s*debugId=(?<debugId>[0-9a-f-]+)\s*$/mu,SOURCE_MAPPING_URL_RE=/\/\/[#@]\s*sourceMappingURL=\S+\s*$/mu,DEBUG_ID_TAIL_BYTES=65536;function readChunkMarkers(text){return{debugId:DEBUG_ID_RE.exec(text)?.groups?.debugId??null,referencesMap:SOURCE_MAPPING_URL_RE.test(text)}}async function readChunk(jsAbs){let tail=await readTail(jsAbs).catch(()=>null),fromTail=tail===null?null:readChunkMarkers(tail);if(fromTail?.debugId)return fromTail;let content=await readFile(jsAbs,`utf8`).catch(()=>null);return content===null?{debugId:null,referencesMap:!1}:readChunkMarkers(content)}async function readTail(jsAbs){let{size}=await stat(jsAbs);if(size<=DEBUG_ID_TAIL_BYTES)return null;let handle=await open(jsAbs,`r`);try{let buf=Buffer.allocUnsafe(DEBUG_ID_TAIL_BYTES),{bytesRead}=await handle.read(buf,0,DEBUG_ID_TAIL_BYTES,size-DEBUG_ID_TAIL_BYTES);return buf.subarray(0,bytesRead).toString(`utf8`)}finally{await handle.close()}}async function discoverTurbopack(opts){let absDistDir=resolveDistDir(opts.projectDir,opts.distDir),relDistDir=relative(opts.projectDir,absDistDir),[mapPaths,jsPaths]=await Promise.all([walkDistTrees(absDistDir,`.js.map`),walkDistTrees(absDistDir,`.js`)]),chunksByDebugId=new Map;await mapWithConcurrency(jsPaths,16,async jsAbs=>{let{debugId,referencesMap}=await readChunk(jsAbs);if(!debugId)return;let chunkUrl=toPublicPath(relative(opts.projectDir,jsAbs),relDistDir),chunk=chunksByDebugId.get(debugId);chunk?(chunk.chunkAbsolutes.push(jsAbs),chunk.referencesMap||=referencesMap):chunksByDebugId.set(debugId,{chunkAbsolutes:[jsAbs],chunkUrl,referencesMap})});let loaded=(await mapWithConcurrency(mapPaths,16,async mapAbs=>{let content=await readFile(mapAbs,`utf8`).catch(()=>null);if(content===null)return null;let debugId;try{let parsed=JSON.parse(content);typeof parsed==`object`&&parsed&&`debugId`in parsed&&typeof parsed.debugId==`string`&&parsed.debugId.length>0&&({debugId}=parsed)}catch{return null}if(!debugId)return null;let chunk=chunksByDebugId.get(debugId);return chunk?{absolute:mapAbs,chunkAbsolutes:chunk.chunkAbsolutes,chunkUrl:chunk.chunkUrl,content,debugId,hash:createHash(`sha256`).update(content).digest(`hex`),path:toPublicPath(relative(opts.projectDir,mapAbs),relDistDir)}:null})).filter(f=>f!==null),loadedDebugIds=new Set(loaded.map(file=>file.debugId));return{files:loaded,missingSourceMapCount:[...chunksByDebugId].filter(([debugId,chunk])=>chunk.referencesMap&&!loadedDebugIds.has(debugId)).length,sourceFileCount:loaded.length}}export{discoverTurbopack};
@@ -6,6 +6,7 @@ export declare function discoverWebpack(opts: {
6
6
  distDir: string;
7
7
  }): Promise<{
8
8
  files: SourceMapFile[];
9
+ missingSourceMapCount: number;
9
10
  sourceFileCount: number;
10
11
  }>;
11
12
  //#endregion
@@ -1 +1 @@
1
- import{resolveDistDir,toPublicPath,walkDistTrees}from"./paths.mjs";import{relative}from"node:path";import{createHash,randomUUID}from"node:crypto";import{injectDebugIdIntoJs,readDebugIdFromJs}from"@interfere/types/data/source-maps";import{mapWithConcurrency}from"@interfere/helpers/map-with-concurrency";import{readFile,writeFile}from"node:fs/promises";const SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(?<sourceMap>\S+)\s*$/u;function injectDebugIdIntoMap(content,debugId){let json=JSON.parse(content);if(typeof json!=`object`||!json||Array.isArray(json))throw TypeError(`Source map must be a JSON object`);return Reflect.set(json,`debugId`,debugId),JSON.stringify(json)}async function discoverWebpack(opts){let absDistDir=resolveDistDir(opts.projectDir,opts.distDir),relDistDir=relative(opts.projectDir,absDistDir),jsPaths=await walkDistTrees(absDistDir,`.js`),discovered=await mapWithConcurrency(jsPaths,16,async jsAbs=>{let content=await readFile(jsAbs,`utf8`),match=SOURCEMAPPING_RE.exec(content);if(!match?.groups?.sourceMap)return null;let mapAbs=`${jsAbs.slice(0,jsAbs.lastIndexOf(`/`)+1)}${decodeURIComponent(match.groups.sourceMap)}`,jsPublic=toPublicPath(relative(opts.projectDir,jsAbs),relDistDir);return[toPublicPath(relative(opts.projectDir,mapAbs),relDistDir),{existingDebugId:readDebugIdFromJs(content),jsAbs,jsContent:content,jsPublic,mapAbs}]}),pairs=new Map;for(let[mapPublic,pair]of discovered.filter(entry=>entry!==null)){let chunkAbsolutes=pairs.get(mapPublic)?.chunkAbsolutes??[];pairs.set(mapPublic,{...pair,chunkAbsolutes:[...chunkAbsolutes,pair.jsAbs]})}return{files:(await Promise.all(Array.from(pairs.entries(),async([mapPublic,pair])=>{let mapContent=await readFile(pair.mapAbs,`utf8`).catch(()=>null);if(mapContent===null)return null;let debugId=pair.existingDebugId??randomUUID(),mapJson=JSON.parse(mapContent),finalMap=typeof mapJson==`object`&&mapJson&&`debugId`in mapJson&&mapJson.debugId===debugId?mapContent:injectDebugIdIntoMap(mapContent,debugId);if(!pair.existingDebugId){let injectedJs=injectDebugIdIntoJs(pair.jsContent,debugId);await writeFile(pair.jsAbs,injectedJs,`utf8`)}return finalMap!==mapContent&&await writeFile(pair.mapAbs,finalMap,`utf8`),{absolute:pair.mapAbs,chunkAbsolutes:pair.chunkAbsolutes,chunkUrl:pair.jsPublic,content:finalMap,debugId,hash:createHash(`sha256`).update(finalMap).digest(`hex`),path:mapPublic}}))).filter(f=>f!==null),sourceFileCount:pairs.size}}export{discoverWebpack,injectDebugIdIntoMap};
1
+ import{resolveDistDir,toPublicPath,walkDistTrees}from"./paths.mjs";import{relative}from"node:path";import{createHash,randomUUID}from"node:crypto";import{injectDebugIdIntoJs,readDebugIdFromJs}from"@interfere/types/data/source-maps";import{mapWithConcurrency}from"@interfere/helpers/map-with-concurrency";import{readFile,writeFile}from"node:fs/promises";const SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(?<sourceMap>\S+)\s*$/u;function injectDebugIdIntoMap(content,debugId){let json=JSON.parse(content);if(typeof json!=`object`||!json||Array.isArray(json))throw TypeError(`Source map must be a JSON object`);return Reflect.set(json,`debugId`,debugId),JSON.stringify(json)}async function discoverWebpack(opts){let absDistDir=resolveDistDir(opts.projectDir,opts.distDir),relDistDir=relative(opts.projectDir,absDistDir),jsPaths=await walkDistTrees(absDistDir,`.js`),discovered=await mapWithConcurrency(jsPaths,16,async jsAbs=>{let content=await readFile(jsAbs,`utf8`),match=SOURCEMAPPING_RE.exec(content);if(!match?.groups?.sourceMap||match.groups.sourceMap.startsWith(`data:`))return null;let mapAbs=`${jsAbs.slice(0,jsAbs.lastIndexOf(`/`)+1)}${decodeURIComponent(match.groups.sourceMap)}`,jsPublic=toPublicPath(relative(opts.projectDir,jsAbs),relDistDir);return[toPublicPath(relative(opts.projectDir,mapAbs),relDistDir),{existingDebugId:readDebugIdFromJs(content),jsAbs,jsContent:content,jsPublic,mapAbs}]}),pairs=new Map;for(let[mapPublic,pair]of discovered.filter(entry=>entry!==null)){let chunkAbsolutes=pairs.get(mapPublic)?.chunkAbsolutes??[];pairs.set(mapPublic,{...pair,chunkAbsolutes:[...chunkAbsolutes,pair.jsAbs]})}let loaded=(await Promise.all(Array.from(pairs.entries(),async([mapPublic,pair])=>{let mapContent=await readFile(pair.mapAbs,`utf8`).catch(()=>null);if(mapContent===null)return null;let debugId=pair.existingDebugId??randomUUID(),mapJson=JSON.parse(mapContent),finalMap=typeof mapJson==`object`&&mapJson&&`debugId`in mapJson&&mapJson.debugId===debugId?mapContent:injectDebugIdIntoMap(mapContent,debugId);if(!pair.existingDebugId){let injectedJs=injectDebugIdIntoJs(pair.jsContent,debugId);await writeFile(pair.jsAbs,injectedJs,`utf8`)}return finalMap!==mapContent&&await writeFile(pair.mapAbs,finalMap,`utf8`),{absolute:pair.mapAbs,chunkAbsolutes:pair.chunkAbsolutes,chunkUrl:pair.jsPublic,content:finalMap,debugId,hash:createHash(`sha256`).update(finalMap).digest(`hex`),path:mapPublic}}))).filter(f=>f!==null);return{files:loaded,missingSourceMapCount:pairs.size-loaded.length,sourceFileCount:pairs.size}}export{discoverWebpack,injectDebugIdIntoMap};
@@ -13,6 +13,7 @@ export interface SourceMapFile {
13
13
  }
14
14
  export interface DiscoveryResult {
15
15
  readonly files: SourceMapFile[];
16
+ readonly missingSourceMapCount: number;
16
17
  readonly sourceFileCount: number;
17
18
  }
18
19
  export declare function discoverSourceMaps(opts: {
@@ -21,6 +22,7 @@ export declare function discoverSourceMaps(opts: {
21
22
  distDir: string;
22
23
  }): Promise<{
23
24
  files: SourceMapFile[];
25
+ missingSourceMapCount: number;
24
26
  sourceFileCount: number;
25
27
  }>;
26
28
  //#endregion
@@ -1,6 +1,6 @@
1
1
  import { SourceMapFile } from "./discover.mjs";
2
2
  //#region src/internal/build/source-maps/guard.d.ts
3
- export declare function assertClientChunksAreMapped(opts: {
3
+ export declare function reportUnmappedClientChunks(opts: {
4
4
  projectDir: string;
5
5
  distDir: string;
6
6
  discovered: readonly SourceMapFile[];
@@ -1 +1 @@
1
- import{log}from"../../logger.mjs";import{resolveDistDir,walkDistTrees}from"./paths.mjs";import{readDebugIdFromJs}from"@interfere/types/data/source-maps";import{readFile}from"node:fs/promises";async function assertClientChunksAreMapped(opts){if(opts.discovered.some(file=>file.chunkUrl.includes(`static/chunks`)))return;let absDistDir=resolveDistDir(opts.projectDir,opts.distDir),clientChunks=(await walkDistTrees(absDistDir,`.js`)).filter(path=>path.replaceAll(`\\`,`/`).includes(`/static/chunks/`)),stampedCount=(await Promise.all(clientChunks.map(async jsAbs=>{let content=await readFile(jsAbs,`utf8`).catch(()=>``);return readDebugIdFromJs(content)!==null}))).filter(Boolean).length;stampedCount!==0&&log.fatal(`Client source maps were not uploaded`,[`${stampedCount} client chunk(s) carry debug IDs but no source maps were uploaded — browser errors from this release won't symbolicate.`,`A coexisting build tool likely deleted them before withInterfere() ran; make withInterfere() the outer wrapper so its discovery runs first, or disable that tool's post-upload map deletion.`])}export{assertClientChunksAreMapped};
1
+ import{log}from"../../logger.mjs";import{resolveDistDir,walkDistTrees}from"./paths.mjs";import{readDebugIdFromJs}from"@interfere/types/data/source-maps";import{readFile}from"node:fs/promises";async function reportUnmappedClientChunks(opts){if(opts.discovered.some(file=>file.chunkUrl.includes(`static/chunks`)))return;let absDistDir=resolveDistDir(opts.projectDir,opts.distDir),clientChunks=(await walkDistTrees(absDistDir,`.js`)).filter(path=>path.replaceAll(`\\`,`/`).includes(`/static/chunks/`)),stampedCount=(await Promise.all(clientChunks.map(async jsAbs=>{let content=await readFile(jsAbs,`utf8`).catch(()=>``);return readDebugIdFromJs(content)!==null}))).filter(Boolean).length;stampedCount!==0&&log.error(`Client source maps were not uploaded`,[`${stampedCount} client chunk(s) carry debug IDs but no source maps were uploaded — browser errors from this release won't symbolicate.`,`A coexisting build tool likely deleted them before withInterfere() ran; make withInterfere() the outer wrapper so its discovery runs first, or disable that tool's post-upload map deletion.`,`The build continues.`])}export{reportUnmappedClientChunks};
@@ -1,2 +1,2 @@
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(`
1
+ import{log}from"../logger.mjs";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";let remoteConfigFallbackLogged=!1;const resolveBuildConfig=createBuildConfigResolver(result=>{result.status!==`fallback`||remoteConfigFallbackLogged||(remoteConfigFallbackLogged=!0,log.warn(`Using default SDK settings`,[`Could not load SDK settings from Interfere. The build continues with the default settings.`]))});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,6 @@
1
1
  //#region src/internal/build-marker-guard.d.ts
2
2
  export declare function buildIntegrationRan(): boolean;
3
+ export declare function interfereDisabled(): boolean;
3
4
  export declare function hasEnvCredentials(): boolean;
4
5
  export declare function warnBuildIntegrationMissing(): void;
5
6
  export declare function warnBuildIntegrationDegraded(): void;
@@ -1 +1 @@
1
- import{BUILD_MARKER_GLOBAL}from"@interfere/types/sdk/injected-globals";function buildIntegrationRan(){let globals=globalThis;return BUILD_MARKER_GLOBAL in globals&&globals[BUILD_MARKER_GLOBAL]!==void 0}function hasEnvCredentials(){return typeof process>`u`?!1:!!process.env.INTERFERE_PUBLIC_KEY||!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE}function warnBuildIntegrationMissing(){console.warn(`[interfere] withInterfere() did not run for this build, so Interfere is inactive and will send no events. Another Next config wrapper can silently drop it when wrapping withInterfere(). Make withInterfere() the outermost wrapper in next.config, or upgrade the wrapping tool.`)}function warnBuildIntegrationDegraded(){console.warn(`[interfere] withInterfere() did not run for this build. Interfere will still send events using credentials from the environment, but release association and sourcemaps are unavailable. Make withInterfere() the outermost wrapper in next.config to restore them.`)}export{buildIntegrationRan,hasEnvCredentials,warnBuildIntegrationDegraded,warnBuildIntegrationMissing};
1
+ import{BUILD_MARKER_GLOBAL,DISABLED_GLOBAL}from"@interfere/types/sdk/injected-globals";function buildIntegrationRan(){let globals=globalThis;return BUILD_MARKER_GLOBAL in globals&&globals[BUILD_MARKER_GLOBAL]!==void 0}function interfereDisabled(){let globals=globalThis;return DISABLED_GLOBAL in globals&&globals[DISABLED_GLOBAL]===!0}function hasEnvCredentials(){return typeof process>`u`?!1:!!process.env.INTERFERE_PUBLIC_KEY||!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE}function warnBuildIntegrationMissing(){console.warn(`[interfere] withInterfere() did not run for this build, so Interfere is inactive and will send no events. Another Next config wrapper can silently drop it when wrapping withInterfere(). Make withInterfere() the outermost wrapper in next.config, or upgrade the wrapping tool.`)}function warnBuildIntegrationDegraded(){console.warn(`[interfere] withInterfere() did not run for this build. Interfere will still send events using credentials from the environment, but release association and sourcemaps are unavailable. Make withInterfere() the outermost wrapper in next.config to restore them.`)}export{buildIntegrationRan,hasEnvCredentials,interfereDisabled,warnBuildIntegrationDegraded,warnBuildIntegrationMissing};
@@ -0,0 +1,3 @@
1
+ //#region src/internal/console-output.d.ts
2
+ export declare function writeConsoleLines(method: unknown, lines: readonly string[]): void;
3
+ //#endregion
@@ -0,0 +1 @@
1
+ function isConsoleMethod(method){return typeof method==`function`}function writeConsoleLines(method,lines){if(isConsoleMethod(method))for(let line of lines)method.call(globalThis.console,line)}export{writeConsoleLines};
@@ -1 +1 @@
1
- import{API_URL}from"@interfere/constants/api";import{ENVIRONMENT_GLOBAL}from"@interfere/types/sdk/injected-globals";import{getStringGlobal}from"@interfere/react/util/global";import{parseEnvValue}from"@interfere/types/sdk/env";import{resolveEnvironment}from"@interfere/types/sdk/runtime";function isEnabledOnServer(){return process.env.NODE_ENV===`production`||!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE}function readInterfereEnv(){let nodeEnvironment=resolveEnvironment({env:process.env,explicit:getStringGlobal(ENVIRONMENT_GLOBAL)});return{apiKey:parseEnvValue(process.env.INTERFERE_API_KEY),apiUrl:parseEnvValue(process.env.INTERFERE_API_URL)??API_URL,nextRuntime:parseEnvValue(process.env.NEXT_RUNTIME),nodeEnvironment,publicKey:parseEnvValue(process.env.INTERFERE_PUBLIC_KEY),release:{destinationId:parseEnvValue(process.env.NEXT_PUBLIC_INTERFERE_RELEASE_ID),sourceId:parseEnvValue(process.env.NEXT_PUBLIC_INTERFERE_BUILD_ID)}}}export{isEnabledOnServer,readInterfereEnv};
1
+ import{interfereDisabled}from"./build-marker-guard.mjs";import{API_URL}from"@interfere/constants/api";import{ENVIRONMENT_GLOBAL}from"@interfere/types/sdk/injected-globals";import{getStringGlobal}from"@interfere/react/util/global";import{parseEnvValue}from"@interfere/types/sdk/env";import{resolveEnvironment}from"@interfere/types/sdk/runtime";function isEnabledOnServer(){return interfereDisabled()?!1:process.env.NODE_ENV===`production`||!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE}function readInterfereEnv(){let nodeEnvironment=resolveEnvironment({env:process.env,explicit:getStringGlobal(ENVIRONMENT_GLOBAL)});return{apiKey:parseEnvValue(process.env.INTERFERE_API_KEY),apiUrl:parseEnvValue(process.env.INTERFERE_API_URL)??API_URL,nextRuntime:parseEnvValue(process.env.NEXT_RUNTIME),nodeEnvironment,publicKey:interfereDisabled()?null:parseEnvValue(process.env.INTERFERE_PUBLIC_KEY),release:{destinationId:parseEnvValue(process.env.NEXT_PUBLIC_INTERFERE_RELEASE_ID),sourceId:parseEnvValue(process.env.NEXT_PUBLIC_INTERFERE_BUILD_ID)}}}export{isEnabledOnServer,readInterfereEnv};
@@ -1 +1 @@
1
- import chalk from"chalk";const styles={error:{content:chalk.redBright,prefix:`${chalk.red.bold(`⨯`)} `,prependLevel:!0,text:chalk.red.bold},fatal:{content:chalk.redBright,prefix:`${chalk.red.bold(`⨯`)} `,prependLevel:!0,text:chalk.red.bold},info:{content:chalk.white,prefix:chalk.whiteBright.bold(`❖`),prependLevel:!1,text:chalk.cyan.bold},warn:{content:chalk.yellowBright,prefix:`${chalk.yellow.bold(`⚠`)} `,prependLevel:!1,text:chalk.yellow.bold}},consoleMethods={error:`error`,fatal:`error`,info:`log`,warn:`warn`};function isTestEnv(){return!!(process.env.VITEST||process.env.VITEST_WORKER_ID)}var FatalError=class extends Error{name=`FatalError`};function emit(level,title,lines){if(isTestEnv())return;let style=styles[level],method=consoleMethods[level],fn=globalThis.console[method];if(typeof fn!=`function`)return;let prependLevel=style.prependLevel?`[${style.text(level.toUpperCase())}] `:``;fn.call(globalThis.console,`${prependLevel} ${chalk.white(`Interfere →`)} ${style.text(title)}`);for(let[i,line]of lines.entries()){let connector=i===lines.length-1?`└`:`├`;fn.call(globalThis.console,`${prependLevel} ${connector} ${style.content(line)}`)}}const log={error:(title,lines)=>{emit(`error`,title,lines)},fatal(title,lines){if(emit(`fatal`,title,lines),isTestEnv())throw new FatalError(title);process.exit(1)},info:(title,lines)=>{emit(`info`,title,lines)},warn:(title,lines)=>{emit(`warn`,title,lines)}};export{FatalError,log};
1
+ import{writeConsoleLines}from"./console-output.mjs";import chalk from"chalk";const styles={error:{content:chalk.redBright,prefix:`${chalk.red.bold(`⨯`)} `,prependLevel:!0,text:chalk.red.bold},fatal:{content:chalk.redBright,prefix:`${chalk.red.bold(`⨯`)} `,prependLevel:!0,text:chalk.red.bold},info:{content:chalk.white,prefix:chalk.whiteBright.bold(`❖`),prependLevel:!1,text:chalk.cyan.bold},warn:{content:chalk.yellowBright,prefix:`${chalk.yellow.bold(`⚠`)} `,prependLevel:!1,text:chalk.yellow.bold}},consoleMethods={error:`error`,fatal:`error`,info:`log`,warn:`warn`};function isTestEnv(){return!!(process.env.VITEST||process.env.VITEST_WORKER_ID)}var FatalError=class extends Error{name=`FatalError`};function emit(level,title,lines){if(isTestEnv())return;let style=styles[level],method=consoleMethods[level],fn=globalThis.console[method],prependLevel=style.prependLevel?`[${style.text(level.toUpperCase())}] `:``,output=[`${prependLevel} ${chalk.white(`Interfere →`)} ${style.text(title)}`,...lines.map((line,index)=>{let connector=index===lines.length-1?`└`:`├`;return`${prependLevel} ${connector} ${style.content(line)}`})];writeConsoleLines(fn,output)}const log={error:(title,lines)=>{emit(`error`,title,lines)},fatal(title,lines){if(emit(`fatal`,title,lines),isTestEnv())throw new FatalError(title);process.exit(1)},info:(title,lines)=>{emit(`info`,title,lines)},warn:(title,lines)=>{emit(`warn`,title,lines)}};export{FatalError,log};
@@ -0,0 +1,12 @@
1
+ import { IdGenerator } from "@opentelemetry/sdk-trace-base";
2
+ //#region src/internal/server/counter-id-generator.d.ts
3
+ export declare class CounterIdGenerator implements IdGenerator {
4
+ private readonly tracePrefix;
5
+ private readonly spanPrefix;
6
+ private traceCounter;
7
+ private spanCounter;
8
+ constructor();
9
+ generateTraceId(): string;
10
+ generateSpanId(): string;
11
+ }
12
+ //#endregion
@@ -0,0 +1 @@
1
+ import{randomBytes}from"node:crypto";const TRACE_COUNTER_MODULUS=2**32,SPAN_COUNTER_MODULUS=2**16;var CounterIdGenerator=class{tracePrefix;spanPrefix;traceCounter=0;spanCounter=0;constructor(){let seed=randomBytes(18);this.tracePrefix=seed.subarray(0,12).toString(`hex`),this.spanPrefix=seed.subarray(12).toString(`hex`)}generateTraceId(){return this.traceCounter=(this.traceCounter+1)%TRACE_COUNTER_MODULUS,this.tracePrefix+this.traceCounter.toString(16).padStart(8,`0`)}generateSpanId(){return this.spanCounter=(this.spanCounter+1)%SPAN_COUNTER_MODULUS,this.spanPrefix+this.spanCounter.toString(16).padStart(4,`0`)}};export{CounterIdGenerator};
@@ -1 +1 @@
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
+ import{exportTracePayload}from"./trace-payload.mjs";import{ExportResultCode}from"@opentelemetry/core";import{InterfereClient}from"@interfere/javascript";import{JsonTraceSerializer}from"@opentelemetry/otlp-transformer";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);exportTracePayload(payload,this.client,resultCallback)}async shutdown(){return Promise.resolve()}async forceFlush(){return Promise.resolve()}};export{FetchTraceExporter};
@@ -1,36 +1,10 @@
1
1
  import { IdGenerator } from "@opentelemetry/sdk-trace-base";
2
2
  //#region src/internal/server/id-generator.d.ts
3
- /**
4
- * IdGenerator hybrid that uses OTel's default `RandomIdGenerator` whenever
5
- * available and falls back to a counter-based generator inside Next 16's
6
- * prerender contexts.
7
- *
8
- * Why: Next 16's prerender extension throws synchronously on any
9
- * `Math.random()` / `crypto.getRandomValues()` call inside a Server
10
- * Component that hasn't first awaited Request data
11
- * (`next-prerender-random` / `next-prerender-crypto`). OTel's default
12
- * generator hits `Math.random()` on every span creation, so spans Next
13
- * opens around prerendered route renders blow up the build.
14
- *
15
- * The hybrid path keeps full 128-bit trace IDs / 64-bit span IDs for the
16
- * common case (SSR, runtime requests, fluid-compute invocations) and only
17
- * degrades inside prerender — where the alternative is build failure.
18
- *
19
- * Inside prerender, IDs are minted from a per-process random prefix
20
- * (seeded once at construction time, outside any prerender ALS frame) plus
21
- * a monotonic counter. Counter widths are masked to keep IDs at their
22
- * spec-mandated lengths even after wrap.
23
- */
3
+ /** Generates random telemetry IDs, with counter IDs when runtime randomness is unavailable. */
24
4
  export declare class PrerenderSafeIdGenerator implements IdGenerator {
25
5
  private readonly random;
26
- private readonly tracePrefix;
27
- private readonly spanPrefix;
28
- private traceCounter;
29
- private spanCounter;
30
- constructor();
6
+ private readonly fallback;
31
7
  generateTraceId(): string;
32
8
  generateSpanId(): string;
33
- private fallbackTraceId;
34
- private fallbackSpanId;
35
9
  }
36
10
  //#endregion
@@ -1 +1 @@
1
- import{RandomIdGenerator}from"@opentelemetry/sdk-trace-base";import{randomBytes}from"node:crypto";const TRACE_COUNTER_MODULUS=2**32,SPAN_COUNTER_MODULUS=2**16;var PrerenderSafeIdGenerator=class{random=new RandomIdGenerator;tracePrefix;spanPrefix;traceCounter=0;spanCounter=0;constructor(){let seed=randomBytes(18);this.tracePrefix=seed.subarray(0,12).toString(`hex`),this.spanPrefix=seed.subarray(12).toString(`hex`)}generateTraceId(){try{return this.random.generateTraceId()}catch{return this.fallbackTraceId()}}generateSpanId(){try{return this.random.generateSpanId()}catch{return this.fallbackSpanId()}}fallbackTraceId(){return this.traceCounter=(this.traceCounter+1)%TRACE_COUNTER_MODULUS,this.tracePrefix+this.traceCounter.toString(16).padStart(8,`0`)}fallbackSpanId(){return this.spanCounter=(this.spanCounter+1)%SPAN_COUNTER_MODULUS,this.spanPrefix+this.spanCounter.toString(16).padStart(4,`0`)}};export{PrerenderSafeIdGenerator};
1
+ import{CounterIdGenerator}from"./counter-id-generator.mjs";import{RandomIdGenerator}from"@opentelemetry/sdk-trace-base";var PrerenderSafeIdGenerator=class{random=new RandomIdGenerator;fallback=new CounterIdGenerator;generateTraceId(){try{return this.random.generateTraceId()}catch{return this.fallback.generateTraceId()}}generateSpanId(){try{return this.random.generateSpanId()}catch{return this.fallback.generateSpanId()}}};export{PrerenderSafeIdGenerator};
@@ -1,3 +1,4 @@
1
+ import { UrlRedactionOption } from "@interfere/types/sdk/url-redaction";
1
2
  import { SpanProcessor } from "@opentelemetry/sdk-trace-base";
2
3
  import { LogRecordProcessor } from "@opentelemetry/sdk-logs";
3
4
  //#region src/internal/server/instrumentation-options.d.ts
@@ -57,6 +58,17 @@ export interface ServerInstrumentationOptions {
57
58
  * opt out to avoid double-emit.
58
59
  */
59
60
  consoleBridge?: boolean;
61
+ /**
62
+ * `urls` controls query and fragment redaction in span and event URL
63
+ * attributes, URL-bearing span names and absolute URLs in exception
64
+ * messages. By default the kit redacts built-in sensitive query values such
65
+ * as `token`; URL credentials are always redacted. The kit's processor
66
+ * redacts while each span ends, so every processor and exporter on the same
67
+ * provider receives the redacted span.
68
+ */
69
+ privacy?: {
70
+ urls?: UrlRedactionOption;
71
+ };
60
72
  /**
61
73
  * Application service name. Defaults to `OTEL_SERVICE_NAME`, then OTel's
62
74
  * `unknown_service:<executable>` or `unknown_service` when unavailable.
@@ -0,0 +1,5 @@
1
+ import { ExportResult } from "@opentelemetry/core";
2
+ import { InterfereClient } from "@interfere/javascript";
3
+ //#region src/internal/server/trace-payload.d.ts
4
+ export declare function exportTracePayload(payload: Uint8Array | null | undefined, client: InterfereClient, resultCallback: (result: ExportResult) => void): void;
5
+ //#endregion
@@ -0,0 +1 @@
1
+ import{ExportResultCode}from"@opentelemetry/core";import{toError}from"@interfere/types/sdk/errors";import{z}from"zod";function exportTracePayload(payload,client,resultCallback){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 client.drain.otlpExport({signal:`traces`,body}),resultCallback({code:ExportResultCode.SUCCESS})}catch(error){resultCallback({code:ExportResultCode.FAILED,error:toError(error)})}})()}export{exportTracePayload};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/next`,version=`12.0.4`;export{name,version};
1
+ var name=`@interfere/next`,version=`12.1.0`;export{name,version};
@@ -4,6 +4,9 @@ import { ConsentCategory, ConsentInput, ConsentInput as ConsentInput$1, ConsentS
4
4
  //#region src/provider.d.ts
5
5
  interface InterfereProviderProps extends PropsWithChildren {
6
6
  consent?: ConsentInput$1 | undefined;
7
+ /** Reports render errors and rethrows to the host boundary. Defaults to true. */
8
+ captureRenderErrors?: boolean;
9
+ /** @deprecated Use captureRenderErrors, which takes precedence when both are set. */
7
10
  errorBoundary?: boolean;
8
11
  plugins?: PluginOverrides | undefined;
9
12
  }
@@ -15,6 +18,6 @@ interface InterfereProviderProps extends PropsWithChildren {
15
18
  * (`init()` is client-only); the core provider mounts with null-safe
16
19
  * accessors and switches to the real kernel on client hydration.
17
20
  */
18
- export declare function InterfereProvider({ children, consent, plugins, errorBoundary }: InterfereProviderProps): import("react").JSX.Element;
21
+ export declare function InterfereProvider({ children, consent, plugins, captureRenderErrors, errorBoundary }: InterfereProviderProps): import("react").JSX.Element;
19
22
  //#endregion
20
23
  export { type ConsentCategory, type ConsentInput, type ConsentState, type GateableCategory, useInterfere, useSession };
package/dist/provider.mjs CHANGED
@@ -1 +1 @@
1
- "use client";import{getKernelOrNull,subscribeToKernel}from"./instrument-client.mjs";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{InterfereProvider as InterfereProvider$1,useInterfere,useSession}from"@interfere/react/provider";import{useSyncExternalStore}from"react";import{jsx}from"react/jsx-runtime";const nullSnapshot=()=>null;function InterfereProvider({children,consent,plugins,errorBoundary}){let kernel=useSyncExternalStore(subscribeToKernel,getKernelOrNull,nullSnapshot);return jsx(InterfereProvider$1,{kernel,...omitUndefined({consent,errorBoundary,plugins}),children})}export{InterfereProvider,useInterfere,useSession};
1
+ "use client";import{getKernelOrNull,subscribeToKernel}from"./instrument-client.mjs";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{InterfereProvider as InterfereProvider$1,useInterfere,useSession}from"@interfere/react/provider";import{useSyncExternalStore}from"react";import{jsx}from"react/jsx-runtime";const nullSnapshot=()=>null;function InterfereProvider({children,consent,plugins,captureRenderErrors,errorBoundary}){let kernel=useSyncExternalStore(subscribeToKernel,getKernelOrNull,nullSnapshot);return jsx(InterfereProvider$1,{kernel,...omitUndefined({captureRenderErrors,consent,errorBoundary,plugins}),children})}export{InterfereProvider,useInterfere,useSession};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/next",
3
- "version": "12.0.4",
3
+ "version": "12.1.0",
4
4
  "description": "Build software that never breaks.",
5
5
  "keywords": [
6
6
  "error-tracking",
@@ -96,12 +96,12 @@
96
96
  "typecheck": "bun check"
97
97
  },
98
98
  "dependencies": {
99
- "@interfere/build": "^0.2.2",
99
+ "@interfere/build": "^0.3.1",
100
100
  "@interfere/constants": "^9.2.0",
101
101
  "@interfere/helpers": "^9.2.0",
102
102
  "@interfere/javascript": "^0.2.1",
103
- "@interfere/react": "^12.0.4",
104
- "@interfere/types": "^10.2.1",
103
+ "@interfere/react": "^12.1.0",
104
+ "@interfere/types": "^10.3.0",
105
105
  "@opentelemetry/api": "^1.9.1",
106
106
  "@opentelemetry/api-logs": "^0.222.0",
107
107
  "@opentelemetry/baggage-span-processor": "^0.5.0",
@@ -130,9 +130,9 @@
130
130
  "@types/react": "19.2.18",
131
131
  "@types/react-dom": "19.2.7",
132
132
  "@vitejs/plugin-react": "^6.1.1",
133
- "@vitest/browser": "5.0.1",
134
- "@vitest/browser-playwright": "5.0.1",
135
- "@vitest/coverage-v8": "5.0.1",
133
+ "@vitest/browser": "5.0.5",
134
+ "@vitest/browser-playwright": "5.0.5",
135
+ "@vitest/coverage-v8": "5.0.5",
136
136
  "msw": "^2.14.6",
137
137
  "next": "^16.3.8",
138
138
  "playwright": "^1.63.0",
@@ -140,7 +140,7 @@
140
140
  "react-dom": "19.3.0",
141
141
  "tsdown": "^0.23.0",
142
142
  "typescript": "^7.0.2",
143
- "vitest": "5.0.1",
143
+ "vitest": "5.0.5",
144
144
  "vitest-browser-react": "2.3.0",
145
145
  "webpack": "^5.111.1"
146
146
  },