@interfere/next 11.0.0-canary.5 → 11.0.0-canary.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  <p align="center">
2
2
  <a href="https://interfere.com">
3
3
  <picture>
4
- <source media="(prefers-color-scheme: dark)" srcset="https://qyzkf4cgb8ydxtq1.public.blob.vercel-storage.com/v2/header/logo-dark.png">
5
- <img src="https://qyzkf4cgb8ydxtq1.public.blob.vercel-storage.com/v2/header/logo-light.png" height="64" alt="Interfere">
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://images.interfere.com/cdn-cgi/imagedelivery/ZQ3TDZdJHXvYNyL5BNulpQ/v2/header/logo-dark/format=auto">
5
+ <img src="https://images.interfere.com/cdn-cgi/imagedelivery/ZQ3TDZdJHXvYNyL5BNulpQ/v2/header/logo-light/format=auto" height="64" alt="Interfere">
6
6
  </picture>
7
7
  </a>
8
8
  <h1 align="center">@interfere/next</h1>
@@ -19,11 +19,7 @@
19
19
  </p>
20
20
 
21
21
  <p align="center">
22
- <a href="https://support.interfere.com/docs">Documentation</a>
23
- ·
24
- <a href="https://support.interfere.com/roadmap">Feature Requests</a>
25
- ·
26
- <a href="https://support.interfere.com/requests">Report a Bug</a>
22
+ <a href="https://interfere.com/docs">Documentation</a>
27
23
  ·
28
24
  <a href="mailto:support@interfere.com">Get Help</a>
29
25
  </p>
@@ -65,14 +61,7 @@ export default withInterfere(config);
65
61
  export { onRequestError, register } from "@interfere/next/instrumentation";
66
62
  ```
67
63
 
68
- **3. Mount the ingest route**
69
-
70
- ```ts
71
- // app/api/interfere/[[...path]]/route.ts
72
- export * from "@interfere/next/route-handler";
73
- ```
74
-
75
- **4. Add the provider**
64
+ **3. Add the provider**
76
65
 
77
66
  ```tsx
78
67
  // app/layout.tsx
@@ -99,38 +88,24 @@ export default function RootLayout({
99
88
  | --- | --- | --- |
100
89
  | `INTERFERE_PUBLIC_KEY` | Yes | Surface publishable key in the `interfere_pub_us_*` or `interfere_pub_eu_*` format. Public by design; used for ingestion. |
101
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. |
102
- | `NEXT_PUBLIC_INTERFERE_ROUTE_PREFIX` | No | Override the proxy route prefix (default: `/api/interfere`). Must match the path you mount the route handler at — e.g. set this to `/__telemetry` and mount the handler at `app/__telemetry/[[...path]]/route.ts`. Read by both the server route handler and the client SDK. |
91
+ | `INTERFERE_API_URL` | No | Override the ingest endpoint in `direct` mode (e.g. a regional endpoint). Defaults to Interfere's endpoint. |
103
92
 
104
93
  ### Migrating From 0.x
105
94
 
106
95
  Upgrade all `@interfere/*` SDK packages together. Keep the env var names, but replace old `int_pub_*` / `interfere_pk_*` values with the dashboard's regional publishable key, and replace old `ak_*` / `interfere_ak_*` build keys with the regional Interfere secret key.
107
96
 
108
- ## Custom Route Prefix
109
-
110
- By default the SDK proxies all telemetry through `/api/interfere`. You can move this to any path by setting `NEXT_PUBLIC_INTERFERE_ROUTE_PREFIX` and mounting the route handler at the matching location.
97
+ ## Ingest Endpoint
111
98
 
112
- **Why change it?** A custom prefix lets you avoid ad-blocker deny-lists that target well-known SDK paths, or align with your own API routing conventions.
113
-
114
- **1. Set the environment variable**
115
-
116
- ```env
117
- NEXT_PUBLIC_INTERFERE_ROUTE_PREFIX=/__telemetry
118
- ```
119
-
120
- **2. Mount the route handler at the matching path**
99
+ By default the SDK posts telemetry to Interfere's endpoint. Ingest requests never carry cookies. Set `apiHost` to a subdomain of your own site that resolves to Interfere via a DNS record — this survives ad-blockers with no server-side component:
121
100
 
122
101
  ```ts
123
- // app/__telemetry/[[...path]]/route.ts
124
- export * from "@interfere/next/route-handler";
102
+ // next.config.ts
103
+ export default withInterfere({
104
+ interfere: { apiHost: "https://in.yourapp.com" },
105
+ });
125
106
  ```
126
107
 
127
- **Normalization rules**
128
-
129
- | Input | Resolved |
130
- | --- | --- |
131
- | `/__telemetry` | `/__telemetry` |
132
- | `__telemetry/` | `/__telemetry` |
133
- | *(empty)* or `/` | `/api/interfere` (default) |
108
+ Add a CNAME record pointing your subdomain at the target shown in your dashboard; Interfere provisions the TLS certificate automatically. To target a regional endpoint instead, set `apiHost` (or the `INTERFERE_API_URL` env) to it.
134
109
 
135
110
  ## Identity Management
136
111
 
package/dist/config.d.mts CHANGED
@@ -3,14 +3,52 @@ import { NextConfig } from "next";
3
3
 
4
4
  //#region src/config.d.ts
5
5
  interface NextConfigWithInterfere extends NextConfig {
6
- /**
7
- * Reserved for future SDK config. Setting `buildId` here is rejected
8
- * loudly — it caused server/client slug drift in 9.x. Override the
9
- * commit SHA via the `INTERFERE_SOURCE_ID` env var (set on both build
10
- * and runtime envs) instead.
11
- */
12
6
  interfere?: {
7
+ /**
8
+ * Reserved. Setting `buildId` here is rejected loudly — it caused
9
+ * server/client slug drift in 9.x. Override the commit SHA via the
10
+ * `INTERFERE_SOURCE_ID` env var (set on both build and runtime envs)
11
+ * instead.
12
+ */
13
13
  readonly buildId?: never;
14
+ /**
15
+ * The endpoint the browser SDK posts telemetry to. Defaults to
16
+ * Interfere's endpoint. Set it to a subdomain of your own site that
17
+ * resolves to Interfere via a DNS record (e.g.
18
+ * `"https://in.yourapp.com"`) to survive ad-blockers, or to a
19
+ * regional endpoint. Must be a full URL.
20
+ */
21
+ readonly apiHost?: string;
22
+ };
23
+ }
24
+ /**
25
+ * Interfere-specific build settings, passed as the first argument to
26
+ * `withInterfere(settings, nextConfig)`. Kept separate from the Next.js
27
+ * config so SDK options never collide with Next's own keys.
28
+ */
29
+ interface NextPluginConfig {
30
+ readonly sourceMaps?: {
31
+ /**
32
+ * Delete source maps from the build output after they've been uploaded
33
+ * to Interfere.
34
+ *
35
+ * Defaults to `true`: maps are uploaded so Interfere can symbolicate
36
+ * stack traces, then removed from the build so your original source is
37
+ * never shipped to production.
38
+ *
39
+ * Set to `false` to keep the maps on disk after upload. Do this when another
40
+ * tool shares the same Next.js build output and runs its own source-map
41
+ * upload from `.next` — Interfere's default purge would delete the maps
42
+ * that tool needs.
43
+ *
44
+ * @default true
45
+ * @example
46
+ * ```ts
47
+ * // Keep maps so another source-map uploader can consume them too:
48
+ * export default withInterfere({ sourceMaps: { purge: false } }, nextConfig);
49
+ * ```
50
+ */
51
+ readonly purge?: boolean;
14
52
  };
15
53
  }
16
54
  interface ResolvedBuildConfig {
@@ -33,6 +71,14 @@ interface ResolvedBuildConfig {
33
71
  */
34
72
  readonly environment: string | null;
35
73
  readonly publicKey: string | null;
74
+ /**
75
+ * When `true` (default), the build pipeline deletes source maps from the
76
+ * build output after uploading them, so they never ship to production.
77
+ * Resolved from `NextPluginConfig.sourceMaps.purge`; set it `false` to
78
+ * leave maps on disk for another uploader that shares the same `.next`
79
+ * output.
80
+ */
81
+ readonly purgeSourceMaps: boolean;
36
82
  /**
37
83
  * Deterministically derived from the leading 16 hex of the commit SHA, so
38
84
  * the build step and the collector arrive at the same
@@ -42,10 +88,11 @@ interface ResolvedBuildConfig {
42
88
  */
43
89
  readonly releaseSlug: ReleaseSlug | null;
44
90
  }
45
- declare function withInterfere(nextConfig?: NextConfigWithInterfere): NextConfig;
91
+ declare function withInterfere(upstream?: NextConfigWithInterfere): NextConfig;
92
+ declare function withInterfere(settings: NextPluginConfig, config: NextConfigWithInterfere): NextConfig;
46
93
  interface ProductionCompileContext {
47
94
  readonly distDir: string;
48
95
  readonly projectDir: string;
49
96
  }
50
97
  //#endregion
51
- export { NextConfigWithInterfere, ProductionCompileContext, ResolvedBuildConfig, withInterfere };
98
+ export { NextConfigWithInterfere, NextPluginConfig, ProductionCompileContext, ResolvedBuildConfig, withInterfere };
package/dist/config.mjs CHANGED
@@ -1 +1 @@
1
- import{configureBuild}from"./internal/build/configure-build.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{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{PHASE_PRODUCTION_BUILD}from"next/constants.js";const CLIENT_TRACE_METADATA_KEYS=[`traceparent`,`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 withInterfere(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(),forceEnable=!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE,build=configureBuild({existingWebpack:webpack,existingTurbopack:turbopack,projectDir,values:{__INTERFERE_RELEASE_SLUG__:forceEnable?LOCAL_DEV_RELEASE_SLUG:config.releaseSlug,...config.publicKey&&{__INTERFERE_PROXY_MODE__:!0,__INTERFERE_PUBLIC_KEY__:config.publicKey},...config.environment&&{__INTERFERE_ENVIRONMENT__:config.environment},...forceEnable&&{__INTERFERE_FORCE_ENABLE__:!0}}});if(config.publicKey!==null&&!build.webpack&&!build.turbopack)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 nextMajor=readNextMajorVersion(projectDir),experimental=interfereExperimentalConfig({existing:rest.experimental,instrumentationHook:nextMajor===null||nextMajor<=14,traceMetadata:config.publicKey!==null||forceEnable}),existingHook=compiler?.runAfterProductionCompile,browserSourceMaps=config.apiKey===null?productionBrowserSourceMaps:!0;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`."]),{...rest,...userEnv?{env:userEnv}:{},compiler:{...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}`])}}},...omitUndefined({productionBrowserSourceMaps:browserSourceMaps,turbopack:build.turbopack,webpack:build.webpack}),...experimental?{experimental}:{}}}function interfereExperimentalConfig({existing,instrumentationHook,traceMetadata}){let merged={...existing,...instrumentationHook?{instrumentationHook:!0}:{},...traceMetadata?{clientTraceMetadata:[...new Set([...existing?.clientTraceMetadata??[],...CLIENT_TRACE_METADATA_KEYS])]}:{}};return Object.keys(merged).length>0?merged:void 0}function resolveBuildConfig(){let{apiKey,apiUrl,publicKey}=readInterfereEnv(),{commitSha,slug}=resolveReleaseSlug(),environment=process.env.NEXT_PHASE===PHASE_PRODUCTION_BUILD?resolveDeployEnvironment(process.env):null;return{apiKey:apiKey??null,apiUrl,buildId:commitSha,environment,publicKey:publicKey??null,releaseSlug:slug}}async function runBuildReleasePipeline(context,config){let{apiKey,buildId,releaseSlug}=config;if(!apiKey)return log.fatal(`API key not set`,[`withInterfere() requires an API 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://support.interfere.com/docs/guides/next-js`]);if(!isInterfereApiKey(apiKey))return reportReleaseDiagnostic(`API_KEY_WRONG_PREFIX`,{apiKey});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.ready){log.warn(`Skipping`,[`No source maps found`]);return}log.info(`Completed`,[`https://interfere.com/~/${result.config?.org.slug}/surfaces/${result.config?.surface.slug}`,`Release: ${result.release?.destination.slug??`Unknown`}`,`Build: ${result.buildId}`,`Artifacts: ${result.fileCount} source maps`])}catch(error){let message=error instanceof Error?error.message:String(error);reportReleaseDiagnostic(classifyReleasePipelineError(error),{errorMessage:message});return}}export{withInterfere};
1
+ import{configureBuild}from"./internal/build/configure-build.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{PHASE_PRODUCTION_BUILD}from"next/constants.js";const CLIENT_TRACE_METADATA_KEYS=[`traceparent`,`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 withInterfere(settingsOrConfig={},maybeConfig){let[settings,nextConfig]=maybeConfig===void 0?[{},settingsOrConfig]:[settingsOrConfig,maybeConfig],{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),apiHost=resolveBrowserApiHost(interfere?.apiHost,config.apiUrl),forceEnable=!!process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE,build=configureBuild({existingWebpack:webpack,existingTurbopack:turbopack,projectDir,values:{__INTERFERE_RELEASE_SLUG__:forceEnable?LOCAL_DEV_RELEASE_SLUG:config.releaseSlug,...config.publicKey&&{__INTERFERE_PUBLIC_KEY__:config.publicKey,...apiHost&&{__INTERFERE_API_URL__:apiHost}},...config.environment&&{__INTERFERE_ENVIRONMENT__:config.environment},...forceEnable&&{__INTERFERE_FORCE_ENABLE__:!0}}});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 nextMajor=readNextMajorVersion(projectDir),experimental=interfereExperimentalConfig({existing:rest.experimental,instrumentationHook:nextMajor===null||nextMajor<=14,traceMetadata:config.publicKey!==null||forceEnable}),existingHook=compiler?.runAfterProductionCompile,browserSourceMaps=config.apiKey===null?productionBrowserSourceMaps:!0;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`."]),{...rest,...userEnv?{env:userEnv}:{},compiler:{...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}`])}}},...omitUndefined({productionBrowserSourceMaps:browserSourceMaps,turbopack:build.turbopack,webpack:build.webpack}),...experimental?{experimental}:{}}}function interfereExperimentalConfig({existing,instrumentationHook,traceMetadata}){let merged={...existing,...instrumentationHook?{instrumentationHook:!0}:{},...traceMetadata?{clientTraceMetadata:[...new Set([...existing?.clientTraceMetadata??[],...CLIENT_TRACE_METADATA_KEYS])]}:{}};return 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){let{apiKey,apiUrl,publicKey}=readInterfereEnv(),{commitSha,slug}=resolveReleaseSlug(),environment=process.env.NEXT_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}}async function runBuildReleasePipeline(context,config){let{apiKey,buildId,releaseSlug}=config;if(!apiKey)return log.fatal(`API key not set`,[`withInterfere() requires an API 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/guides/next-js`]);if(!isInterfereApiKey(apiKey))return reportReleaseDiagnostic(`API_KEY_WRONG_PREFIX`,{apiKey});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.ready){log.warn(`Skipping`,[`No source maps found`]);return}log.info(`Completed`,[`https://interfere.com/~/${result.config?.org.slug}/surfaces/${result.config?.surface.slug}`,`Release: ${result.release?.destination.slug??`Unknown`}`,`Build: ${result.buildId}`,`Artifacts: ${result.fileCount} source maps`])}catch(error){let message=error instanceof Error?error.message:String(error);reportReleaseDiagnostic(classifyReleasePipelineError(error),{errorMessage:message});return}}export{withInterfere};
@@ -1 +1 @@
1
- import{readInterfereEnv}from"./internal/env.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{FetchTraceExporter}from"./internal/server/edge-exporter.mjs";import{withPublicKey}from"./internal/url.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{LOCAL_DEV_RELEASE_SLUG}from"@interfere/types/releases/slug";import{trace}from"@opentelemetry/api";import{resourceFromAttributes}from"@opentelemetry/resources";import{BasicTracerProvider,SimpleSpanProcessor}from"@opentelemetry/sdk-trace-base";let registered=!1;function register(opts={}){if(registered)return Promise.resolve();let env=readInterfereEnv();if(!env.publicKey)return Promise.resolve();let releaseSlug=process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE?LOCAL_DEV_RELEASE_SLUG:null;releaseSlug||console.warn(`[interfere] edge runtime has no build-baked release.slug; the collector will reject edge spans until one is wired (production follow-up).`);let sinkUrl=withPublicKey(`${env.apiUrl}/v2/sink`,env.publicKey),provider=new BasicTracerProvider({resource:resourceFromAttributes({"service.name":opts.serviceName??`interfere-sdk-next-edge`,"service.namespace":`interfere`,"deployment.environment.name":env.nodeEnvironment,"telemetry.sdk.language":`nodejs`,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,...releaseSlug?{"release.slug":releaseSlug}:{}}),spanProcessors:[new SimpleSpanProcessor(new FetchTraceExporter(sinkUrl,{"x-interfere-producer-version":PRODUCER_VERSION}))]});return trace.setGlobalTracerProvider(provider),registered=!0,Promise.resolve()}function buildInterfereOtelKit(_opts={}){return null}export{buildInterfereOtelKit,onRequestError,register};
1
+ import{readInterfereEnv}from"./internal/env.mjs";import{PRODUCER_VERSION}from"./internal/version.mjs";import{FetchTraceExporter}from"./internal/server/edge-exporter.mjs";import{setServerFlush}from"./internal/server/server-flush.mjs";import{withPublicKey}from"./internal/url.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{LOCAL_DEV_RELEASE_SLUG}from"@interfere/types/releases/slug";import{trace}from"@opentelemetry/api";import{resourceFromAttributes}from"@opentelemetry/resources";import{BasicTracerProvider,SimpleSpanProcessor}from"@opentelemetry/sdk-trace-base";let registered=!1;function register(opts={}){if(registered)return Promise.resolve();let env=readInterfereEnv();if(!env.publicKey)return Promise.resolve();let releaseSlug=process.env.NEXT_PUBLIC_INTERFERE_FORCE_ENABLE?LOCAL_DEV_RELEASE_SLUG:null;releaseSlug||console.warn(`[interfere] edge runtime has no build-baked release.slug; the collector will reject edge spans until one is wired (production follow-up).`);let sinkUrl=withPublicKey(`${env.apiUrl}/v2/sink`,env.publicKey),provider=new BasicTracerProvider({resource:resourceFromAttributes({"service.name":opts.serviceName??`interfere-sdk-next-edge`,"service.namespace":`interfere`,"deployment.environment.name":env.nodeEnvironment,"telemetry.sdk.language":`nodejs`,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,...releaseSlug?{"release.slug":releaseSlug}:{}}),spanProcessors:[new SimpleSpanProcessor(new FetchTraceExporter(sinkUrl,{"x-interfere-producer-version":PRODUCER_VERSION}))]});return trace.setGlobalTracerProvider(provider),setServerFlush(()=>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{withPublicKey}from"./internal/url.mjs";import{fetchAndCacheRemoteConfig}from"./internal/server/remote-config.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{bridgeConsoleToOtel}from"./internal/server/console-bridge.mjs";import{installGlobalHandlers}from"./internal/server/global-handlers.mjs";import{PrerenderSafeIdGenerator}from"./internal/server/id-generator.mjs";import{metrics,propagation}from"@opentelemetry/api";import{resourceFromAttributes}from"@opentelemetry/resources";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{OTLPLogExporter}from"@opentelemetry/exporter-logs-otlp-http";import{AggregationTemporalityPreference,OTLPMetricExporter}from"@opentelemetry/exporter-metrics-otlp-http";import{OTLPTraceExporter}from"@opentelemetry/exporter-trace-otlp-http";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{UndiciInstrumentation}from"@opentelemetry/instrumentation-undici";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";let registered=!1,tracerProvider=null,loggerProvider=null,meterProvider=null;async function flush(){await Promise.all([tracerProvider?.forceFlush(),loggerProvider?.forceFlush(),meterProvider?.forceFlush()])}function matchesAny(url,patterns){for(let pattern of patterns)if(pattern instanceof RegExp?pattern.test(url):url.includes(pattern))return!0;return!1}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 sinkUrl=withPublicKey(`${env.apiUrl}/v2/sink`,env.publicKey),ignoreUrls=[sinkUrl,...opts.ignoreUrls??[]],propagateContextUrls=opts.propagateContextUrls??[],exporterHeaders={"x-interfere-producer-version":PRODUCER_VERSION};return{resourceAttributes:{"service.name":opts.serviceName??`interfere-sdk-next-server`,"service.namespace":`interfere`,"deployment.environment.name":env.nodeEnvironment,"telemetry.sdk.language":`nodejs`,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,...releaseSlug?{"release.slug":releaseSlug}:{}},spanProcessors:[new BaggageSpanProcessor(()=>!0),new BatchSpanProcessor(new OTLPTraceExporter({url:sinkUrl,headers:exporterHeaders})),...opts._internalAdditionalSpanProcessors??[]],logRecordProcessors:[new BatchLogRecordProcessor(new OTLPLogExporter({url:sinkUrl,headers:exporterHeaders})),...opts._internalAdditionalLogRecordProcessors??[]],metricReaders:[new PeriodicExportingMetricReader({exporter:new OTLPMetricExporter({url:sinkUrl,headers:exporterHeaders,temporalityPreference:AggregationTemporalityPreference.DELTA}),exportIntervalMillis:3e4}),...opts._internalAdditionalMetricReaders??[]],propagators:[new W3CBaggagePropagator],instrumentations:[new UndiciInstrumentation({ignoreRequestHook:req=>matchesAny(`${req.origin}${req.path}`,ignoreUrls),requestHook:(span,req)=>{matchesAny(`${req.origin}${req.path}`,propagateContextUrls)&&span.setAttribute(`interfere.propagated`,!0)}})],enableConsoleBridge:()=>bridgeConsoleToOtel(),fetchRemoteConfig:()=>fetchAndCacheRemoteConfig()}}async function register(opts={}){if(registered)return;let kit=buildInterfereOtelKit(opts);if(!kit)return;let resource=resourceFromAttributes(kit.resourceAttributes);tracerProvider=new NodeTracerProvider({resource,spanProcessors:kit.spanProcessors,idGenerator:new PrerenderSafeIdGenerator}),propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,...kit.propagators]})),tracerProvider.register({contextManager:new AsyncLocalStorageContextManager().enable()}),loggerProvider=new LoggerProvider({resource,processors:kit.logRecordProcessors});let{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider),meterProvider=new MeterProvider({resource,readers:kit.metricReaders}),metrics.setGlobalMeterProvider(meterProvider),registerInstrumentations({tracerProvider,meterProvider,instrumentations:kit.instrumentations}),opts.consoleBridge!==!1&&kit.enableConsoleBridge(),installGlobalHandlers({captureUncaughtException:opts.captureUncaughtException??!0,captureUnhandledRejection:opts.captureUnhandledRejection??!0,exitOnUncaughtException:process.env.NODE_ENV===`production`,flush}),registered=!0,await kit.fetchRemoteConfig()}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{withPublicKey}from"./internal/url.mjs";import{fetchAndCacheRemoteConfig}from"./internal/server/remote-config.mjs";import{onRequestError}from"./internal/server/capture.mjs";import{bridgeConsoleToOtel}from"./internal/server/console-bridge.mjs";import{installGlobalHandlers}from"./internal/server/global-handlers.mjs";import{PrerenderSafeIdGenerator}from"./internal/server/id-generator.mjs";import{metrics,propagation}from"@opentelemetry/api";import{resourceFromAttributes}from"@opentelemetry/resources";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{OTLPLogExporter}from"@opentelemetry/exporter-logs-otlp-http";import{AggregationTemporalityPreference,OTLPMetricExporter}from"@opentelemetry/exporter-metrics-otlp-http";import{OTLPTraceExporter}from"@opentelemetry/exporter-trace-otlp-http";import{registerInstrumentations}from"@opentelemetry/instrumentation";import{UndiciInstrumentation}from"@opentelemetry/instrumentation-undici";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";let registered=!1,tracerProvider=null,loggerProvider=null,meterProvider=null;async function flush(){await Promise.all([tracerProvider?.forceFlush(),loggerProvider?.forceFlush(),meterProvider?.forceFlush()])}function matchesAny(url,patterns){for(let pattern of patterns)if(pattern instanceof RegExp?pattern.test(url):url.includes(pattern))return!0;return!1}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 sinkUrl=withPublicKey(`${env.apiUrl}/v2/sink`,env.publicKey),ignoreUrls=[sinkUrl,...opts.ignoreUrls??[]],propagateContextUrls=opts.propagateContextUrls??[],exporterHeaders={"x-interfere-producer-version":PRODUCER_VERSION};return{resourceAttributes:{"service.name":opts.serviceName??`interfere-sdk-next-server`,"service.namespace":`interfere`,"deployment.environment.name":env.nodeEnvironment,"telemetry.sdk.language":`nodejs`,"interfere.sdk.name":`@interfere/next`,"interfere.sdk.version":PRODUCER_VERSION,...releaseSlug?{"release.slug":releaseSlug}:{}},spanProcessors:[new BaggageSpanProcessor(()=>!0),new BatchSpanProcessor(new OTLPTraceExporter({url:sinkUrl,headers:exporterHeaders})),...opts._internalAdditionalSpanProcessors??[]],logRecordProcessors:[new BatchLogRecordProcessor(new OTLPLogExporter({url:sinkUrl,headers:exporterHeaders})),...opts._internalAdditionalLogRecordProcessors??[]],metricReaders:[new PeriodicExportingMetricReader({exporter:new OTLPMetricExporter({url:sinkUrl,headers:exporterHeaders,temporalityPreference:AggregationTemporalityPreference.DELTA}),exportIntervalMillis:3e4}),...opts._internalAdditionalMetricReaders??[]],propagators:[new W3CBaggagePropagator],instrumentations:[new UndiciInstrumentation({ignoreRequestHook:req=>matchesAny(`${req.origin}${req.path}`,ignoreUrls),requestHook:(span,req)=>{matchesAny(`${req.origin}${req.path}`,propagateContextUrls)&&span.setAttribute(`interfere.propagated`,!0)}})],enableConsoleBridge:()=>bridgeConsoleToOtel(),fetchRemoteConfig:()=>fetchAndCacheRemoteConfig()}}async function register(opts={}){if(registered)return;let kit=buildInterfereOtelKit(opts);if(!kit)return;let resource=resourceFromAttributes(kit.resourceAttributes);tracerProvider=new NodeTracerProvider({resource,spanProcessors:kit.spanProcessors,idGenerator:new PrerenderSafeIdGenerator}),propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,...kit.propagators]})),tracerProvider.register({contextManager:new AsyncLocalStorageContextManager().enable()}),loggerProvider=new LoggerProvider({resource,processors:kit.logRecordProcessors});let{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider),meterProvider=new MeterProvider({resource,readers:kit.metricReaders}),metrics.setGlobalMeterProvider(meterProvider),registerInstrumentations({tracerProvider,meterProvider,instrumentations:kit.instrumentations}),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,await kit.fetchRemoteConfig()}export{PrerenderSafeIdGenerator,buildInterfereOtelKit,flush,onRequestError,register};
@@ -2,9 +2,9 @@ import { NextConfig } from "next";
2
2
 
3
3
  //#region src/internal/build/configure-build.d.ts
4
4
  interface InterfereInjectedValues {
5
+ readonly __INTERFERE_API_URL__?: string;
5
6
  readonly __INTERFERE_ENVIRONMENT__?: string;
6
7
  readonly __INTERFERE_FORCE_ENABLE__?: boolean;
7
- readonly __INTERFERE_PROXY_MODE__?: boolean;
8
8
  readonly __INTERFERE_PUBLIC_KEY__?: string;
9
9
  readonly __INTERFERE_RELEASE_SLUG__: string | null;
10
10
  }
@@ -15,6 +15,7 @@ interface ConfigureBuildInput {
15
15
  readonly values: InterfereInjectedValues;
16
16
  }
17
17
  interface ConfigureBuildOutput {
18
+ readonly clientInstrumentationApplies: boolean;
18
19
  readonly turbopack: NextConfig["turbopack"] | undefined;
19
20
  readonly webpack: NextConfig["webpack"] | undefined;
20
21
  }
@@ -1 +1 @@
1
- import{existsSync}from"node:fs";import{basename,dirname,join,normalize,resolve}from"node:path";import{fileURLToPath}from"node:url";const INSTRUMENTATION_CLIENT_FILES=[`instrumentation-client.ts`,`instrumentation-client.tsx`,`instrumentation-client.js`,`instrumentation-client.jsx`,`src/instrumentation-client.ts`,`src/instrumentation-client.tsx`,`src/instrumentation-client.js`,`src/instrumentation-client.jsx`],INSTRUMENTATION_SERVER_FILES=[`instrumentation.ts`,`instrumentation.tsx`,`instrumentation.js`,`instrumentation.jsx`,`src/instrumentation.ts`,`src/instrumentation.tsx`,`src/instrumentation.js`,`src/instrumentation.jsx`],LOADER_FILE_CANDIDATES=[`value-injection-loader.ts`,`value-injection-loader.js`,`value-injection-loader.mjs`,`value-injection-loader.cjs`];function configureBuild({existingWebpack,existingTurbopack,projectDir,values}){let clientPath=resolveFirstExisting(projectDir,INSTRUMENTATION_CLIENT_FILES),targets=[clientPath,resolveFirstExisting(projectDir,INSTRUMENTATION_SERVER_FILES)].filter(path=>path!==null);return{webpack:buildWebpackHook(existingWebpack,clientPath,targets,values),turbopack:buildTurbopackConfig(existingTurbopack,clientPath,targets,values)}}function hasInjectedMetadata(metadata){return metadata.__INTERFERE_RELEASE_SLUG__!==null||metadata.__INTERFERE_PUBLIC_KEY__!==void 0}function buildWebpackHook(existingWebpack,clientPath,targets,values){if(!(clientPath&&hasInjectedMetadata(values)))return existingWebpack;let normalizedTargets=new Set(targets.map(path=>normalize(path))),loaderPath=resolveLoaderPath();return(webpackConfig,options)=>{let outputConfig=existingWebpack?existingWebpack(webpackConfig,options):webpackConfig,mutableConfig=outputConfig,injectionRule={test:resource=>!!resource&&normalizedTargets.has(normalize(resource)),use:[{loader:loaderPath,options:{values}}]};return mutableConfig.module??={},mutableConfig.module.rules??=[],mutableConfig.module.rules.push(injectionRule),outputConfig}}function buildTurbopackConfig(existingTurbopack,clientPath,targets,values){let baseConfig=existingTurbopack??{},debugIds=baseConfig.debugIds??!0;if(!(clientPath&&hasInjectedMetadata(values)))return existingTurbopack===void 0&&debugIds===baseConfig.debugIds?existingTurbopack:{...baseConfig,debugIds};let loaderPath=resolveLoaderPath(),rules={...baseConfig.rules};for(let target of targets){let ruleKey=`**/${basename(target)}`,existingRule=rules[ruleKey]??{},existingLoaders=Array.isArray(existingRule.loaders)?existingRule.loaders:[];rules[ruleKey]={...existingRule,loaders:[...existingLoaders,{loader:loaderPath,options:{serializedValues:JSON.stringify(values)}}]}}return{...baseConfig,debugIds,rules}}function resolveFirstExisting(projectDir,candidates){for(let candidate of candidates){let filePath=resolve(projectDir,candidate);if(existsSync(filePath))return filePath}return null}function resolveLoaderPath(){let directory=resolve(dirname(fileURLToPath(import.meta.url)));for(let candidate of LOADER_FILE_CANDIDATES){let filePath=join(directory,candidate);if(existsSync(filePath))return filePath}return join(directory,`value-injection-loader.js`)}export{configureBuild};
1
+ import{existsSync}from"node:fs";import{basename,dirname,join,normalize,resolve}from"node:path";import{fileURLToPath}from"node:url";const INSTRUMENTATION_CLIENT_FILES=[`instrumentation-client.ts`,`instrumentation-client.tsx`,`instrumentation-client.js`,`instrumentation-client.jsx`,`src/instrumentation-client.ts`,`src/instrumentation-client.tsx`,`src/instrumentation-client.js`,`src/instrumentation-client.jsx`],INSTRUMENTATION_SERVER_FILES=[`instrumentation.ts`,`instrumentation.tsx`,`instrumentation.js`,`instrumentation.jsx`,`src/instrumentation.ts`,`src/instrumentation.tsx`,`src/instrumentation.js`,`src/instrumentation.jsx`],LOADER_FILE_CANDIDATES=[`value-injection-loader.ts`,`value-injection-loader.js`,`value-injection-loader.mjs`,`value-injection-loader.cjs`];function configureBuild({existingWebpack,existingTurbopack,projectDir,values}){let clientPath=resolveFirstExisting(projectDir,INSTRUMENTATION_CLIENT_FILES),targets=[clientPath,resolveFirstExisting(projectDir,INSTRUMENTATION_SERVER_FILES)].filter(path=>path!==null),clientInstrumentationApplies=!!(clientPath&&hasInjectedMetadata(values));return{clientInstrumentationApplies,webpack:buildWebpackHook(existingWebpack,targets,values,clientInstrumentationApplies),turbopack:buildTurbopackConfig(existingTurbopack,targets,values,clientInstrumentationApplies)}}function hasInjectedMetadata(metadata){return metadata.__INTERFERE_RELEASE_SLUG__!==null||metadata.__INTERFERE_PUBLIC_KEY__!==void 0}function buildWebpackHook(existingWebpack,targets,values,injectionApplies){if(!injectionApplies)return existingWebpack;let normalizedTargets=new Set(targets.map(path=>normalize(path))),injectionLoaderPath=resolveLoaderPath();return(webpackConfig,options)=>{let outputConfig=existingWebpack?existingWebpack(webpackConfig,options):webpackConfig,mutableConfig=outputConfig;return mutableConfig.module??={},mutableConfig.module.rules??=[],mutableConfig.module.rules.push({test:resource=>!!resource&&normalizedTargets.has(normalize(resource)),use:[{loader:injectionLoaderPath,options:{values}}]}),outputConfig}}function buildTurbopackConfig(existingTurbopack,targets,values,injectionApplies){let baseConfig=existingTurbopack??{},debugIds=baseConfig.debugIds??!0;if(!injectionApplies)return existingTurbopack===void 0&&debugIds===baseConfig.debugIds?existingTurbopack:{...baseConfig,debugIds};let rules={...baseConfig.rules},loaderPath=resolveLoaderPath();for(let target of targets){let ruleKey=`**/${basename(target)}`;rules[ruleKey]=appendTurbopackLoader(rules[ruleKey],{loader:loaderPath,options:{serializedValues:JSON.stringify(values)}})}return{...baseConfig,debugIds,rules}}function appendTurbopackLoader(existing,loader){let rule=existing??{},loaders=Array.isArray(rule.loaders)?rule.loaders:[];return{...rule,loaders:[...loaders,loader]}}function resolveFirstExisting(projectDir,candidates){for(let candidate of candidates){let filePath=resolve(projectDir,candidate);if(existsSync(filePath))return filePath}return null}function resolveLoaderPath(){let directory=resolve(dirname(fileURLToPath(import.meta.url)));for(let candidate of LOADER_FILE_CANDIDATES){let filePath=join(directory,candidate);if(existsSync(filePath))return filePath}return join(directory,`value-injection-loader.js`)}export{configureBuild};
@@ -1 +1 @@
1
- import{detectActiveBundler}from"./detect-bundler.mjs";import{resolveReleaseRequest}from"./release/index.mjs";import{discoverSourceMaps}from"./source-maps/discover.mjs";import{uploadSourceMaps}from"./source-maps/upload.mjs";import{cleanupSourceMaps}from"./source-maps/index.mjs";import{releaseSlugSchema}from"@interfere/types/releases/slug";import{HTTPClient,Interfere}from"@interfere/sdk";async function timed(fn){let start=performance.now();return[await fn(),Math.round(performance.now()-start)]}async function runBuildPipeline(context,metadata){let{apiUrl,apiKey}=metadata,httpClient=new HTTPClient;httpClient.addHook(`beforeRequest`,request=>{let nextRequest=new Request(request);return nextRequest.headers.set(`Authorization`,`Bearer ${apiKey}`),nextRequest});let sdk=new Interfere({serverURL:apiUrl,httpClient}),security={apiKey},start=performance.now(),bundler=detectActiveBundler(),[{discovered,config},discoverMs]=await timed(async()=>{let[discovered,config]=await Promise.all([discoverSourceMaps({bundler,projectDir:context.projectDir,distDir:context.distDir}),sdk.releases.getConfig(security)]);return{discovered,config}});if(discovered.files.length===0)return{ready:!1,reason:`no_source_maps`,fileCount:0};let releaseRequest=resolveReleaseRequest(metadata.buildId,metadata.environment,metadata.releaseSlug,config),[release,createReleaseMs]=await timed(()=>sdk.releases.create(security,releaseRequest)),releaseSlug=releaseSlugSchema.parse(release.destination.slug),buildId=release.build.hash??metadata.buildId,[{totalBytes},uploadMs]=await timed(()=>uploadSourceMaps({apiUrl,bundler,discovered,httpClient,releaseSlug})),[,preflightMs]=await timed(()=>sdk.releases.preflight(security,{releaseSlug})),[,cleanupMs]=await timed(()=>cleanupSourceMaps(discovered.files)),timing={discover:discoverMs,createRelease:createReleaseMs,upload:uploadMs,preflight:preflightMs,cleanup:cleanupMs,total:Math.round(performance.now()-start),fileCount:discovered.files.length,totalBytes};return{ready:!0,fileCount:discovered.files.length,release:{...release,destination:{...release.destination,slug:releaseSlug}},config,buildId,timing}}export{runBuildPipeline};
1
+ import{detectActiveBundler}from"./detect-bundler.mjs";import{resolveReleaseRequest}from"./release/index.mjs";import{discoverSourceMaps}from"./source-maps/discover.mjs";import{uploadSourceMaps}from"./source-maps/upload.mjs";import{cleanupSourceMaps}from"./source-maps/index.mjs";import{releaseSlugSchema}from"@interfere/types/releases/slug";import{HTTPClient,Interfere}from"@interfere/sdk";async function timed(fn){let start=performance.now();return[await fn(),Math.round(performance.now()-start)]}async function runBuildPipeline(context,metadata){let{apiUrl,apiKey}=metadata,httpClient=new HTTPClient;httpClient.addHook(`beforeRequest`,request=>{let nextRequest=new Request(request);return nextRequest.headers.set(`Authorization`,`Bearer ${apiKey}`),nextRequest});let sdk=new Interfere({serverURL:apiUrl,httpClient}),security={apiKey},start=performance.now(),bundler=detectActiveBundler(),[{discovered,config},discoverMs]=await timed(async()=>{let[discovered,config]=await Promise.all([discoverSourceMaps({bundler,projectDir:context.projectDir,distDir:context.distDir}),sdk.releases.getConfig(security)]);return{discovered,config}});if(discovered.files.length===0)return{ready:!1,reason:`no_source_maps`,fileCount:0};let releaseRequest=resolveReleaseRequest(metadata.buildId,metadata.environment,metadata.releaseSlug,config),[release,createReleaseMs]=await timed(()=>sdk.releases.create(security,releaseRequest)),releaseSlug=releaseSlugSchema.parse(release.destination.slug),buildId=release.build.hash??metadata.buildId,[{totalBytes},uploadMs]=await timed(()=>uploadSourceMaps({apiUrl,bundler,discovered,httpClient,releaseSlug})),[,preflightMs]=await timed(()=>sdk.releases.preflight(security,{releaseSlug})),[,cleanupMs]=await timed(async()=>{metadata.purgeSourceMaps&&await cleanupSourceMaps(discovered.files)}),timing={discover:discoverMs,createRelease:createReleaseMs,upload:uploadMs,preflight:preflightMs,cleanup:cleanupMs,total:Math.round(performance.now()-start),fileCount:discovered.files.length,totalBytes};return{ready:!0,fileCount:discovered.files.length,release:{...release,destination:{...release.destination,slug:releaseSlug}},config,buildId,timing}}export{runBuildPipeline};
@@ -1,41 +1,7 @@
1
1
  import { SourceMapFile } from "./discover.mjs";
2
2
 
3
3
  //#region src/internal/build/source-maps/discover-webpack.d.ts
4
- /**
5
- * Inserts a `//# debugId=…` comment into the chunk. Idempotent: if a
6
- * spec-compliant comment is already present (e.g. webpack 5.104+ with
7
- * `SourceMapDevToolPlugin({ debugIds: true })`, turbopack with
8
- * `experimental.turbopack.debugIds`, or any other tool) it's
9
- * overwritten with the supplied debugId — callers that read the
10
- * existing id via `readDebugIdFromJs` first get an idempotent no-op.
11
- *
12
- * Otherwise the comment is appended just before the existing
13
- * `//# sourceMappingURL=…` line so the browser still resolves the map.
14
- */
15
- declare function injectDebugIdIntoJs(content: string, debugId: string): string;
16
- /**
17
- * Adds `debugId` to the source-map JSON. Idempotent: writes both
18
- * `debugId` (TC39 spec field) and `debug_id` (legacy variant some
19
- * older Sentry tooling emits) so manifests stay readable across the
20
- * ecosystem. Re-running with the same id produces identical output.
21
- */
22
4
  declare function injectDebugIdIntoMap(content: string, debugId: string): string;
23
- /**
24
- * Webpack-mode discovery: walks the dist tree, parses each `.js`
25
- * chunk's `//# sourceMappingURL=…` comment to find its paired `.map`,
26
- * and ensures both carry the same `debugId`.
27
- *
28
- * Idempotent w.r.t. debug-id state on disk:
29
- * - If the chunk already carries a `//# debugId=…` comment (webpack
30
- * 5.104+ with `SourceMapDevToolPlugin({ debugIds: true })`, or a
31
- * third-party tool that already injected one) we reuse it.
32
- * - Otherwise we generate a fresh UUID and inject it into both files.
33
- *
34
- * The "reuse if present" branch matches the strategy in Sentry's
35
- * webpack plugin (`addDebugIdToBundleSource` in
36
- * `bundler-plugin-core/src/debug-id-upload.ts`) and means our
37
- * discovery works transparently when bundlers natively emit debugIds.
38
- */
39
5
  declare function discoverWebpack(opts: {
40
6
  projectDir: string;
41
7
  distDir: string;
@@ -44,4 +10,4 @@ declare function discoverWebpack(opts: {
44
10
  sourceFileCount: number;
45
11
  }>;
46
12
  //#endregion
47
- export { discoverWebpack, injectDebugIdIntoJs, injectDebugIdIntoMap };
13
+ export { discoverWebpack, injectDebugIdIntoMap };
@@ -1 +1 @@
1
- import{resolveDistDir,toPublicPath,walkDistTrees}from"./paths.mjs";import{relative}from"node:path";import{createHash,randomUUID}from"node:crypto";import{readFile,writeFile}from"node:fs/promises";const SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/,SPEC_LAST_DEBUG_ID_RE=/\/\/# debugId=([a-fA-F0-9-]+)(?![\s\S]*\/\/# debugId=)/m;function readDebugIdFromJs(content){return SPEC_LAST_DEBUG_ID_RE.exec(content)?.[1]??null}function injectDebugIdIntoJs(content,debugId){let debugIdComment=`//# debugId=${debugId}`;if(SPEC_LAST_DEBUG_ID_RE.test(content))return content.replace(SPEC_LAST_DEBUG_ID_RE,debugIdComment);let match=SOURCEMAPPING_RE.exec(content);return match?`${content.slice(0,match.index).trimEnd()}\n${debugIdComment}\n${match[0]}`:`${content}\n${debugIdComment}`}function injectDebugIdIntoMap(content,debugId){let json=JSON.parse(content);return 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`),pairs=new Map;for(let jsAbs of jsPaths){let content=await readFile(jsAbs,`utf8`),match=SOURCEMAPPING_RE.exec(content);if(!match?.[1])continue;let mapAbs=`${jsAbs.slice(0,jsAbs.lastIndexOf(`/`)+1)}${decodeURIComponent(match[1])}`,jsPublic=toPublicPath(relative(opts.projectDir,jsAbs),relDistDir),mapPublic=toPublicPath(relative(opts.projectDir,mapAbs),relDistDir);pairs.set(mapPublic,{mapAbs,jsAbs,jsPublic,jsContent:content,existingDebugId:readDebugIdFromJs(content)})}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();if(!pair.existingDebugId){let injectedJs=injectDebugIdIntoJs(pair.jsContent,debugId);await writeFile(pair.jsAbs,injectedJs,`utf8`)}let finalMap=JSON.parse(mapContent).debugId===debugId?mapContent:injectDebugIdIntoMap(mapContent,debugId);return finalMap!==mapContent&&await writeFile(pair.mapAbs,finalMap,`utf8`),{absolute:pair.mapAbs,path:mapPublic,content:finalMap,hash:createHash(`sha256`).update(finalMap).digest(`hex`),debugId,chunkUrl:pair.jsPublic}}))).filter(f=>f!==null),sourceFileCount:pairs.size}}export{discoverWebpack,injectDebugIdIntoJs,injectDebugIdIntoMap};
1
+ import{resolveDistDir,toPublicPath,walkDistTrees}from"./paths.mjs";import{relative}from"node:path";import{createHash,randomUUID}from"node:crypto";import{readFile,writeFile}from"node:fs/promises";import{injectDebugIdIntoJs,readDebugIdFromJs}from"@interfere/types/data/source-maps";const SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/;function injectDebugIdIntoMap(content,debugId){let json=JSON.parse(content);return 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`),pairs=new Map;for(let jsAbs of jsPaths){let content=await readFile(jsAbs,`utf8`),match=SOURCEMAPPING_RE.exec(content);if(!match?.[1])continue;let mapAbs=`${jsAbs.slice(0,jsAbs.lastIndexOf(`/`)+1)}${decodeURIComponent(match[1])}`,jsPublic=toPublicPath(relative(opts.projectDir,jsAbs),relDistDir),mapPublic=toPublicPath(relative(opts.projectDir,mapAbs),relDistDir);pairs.set(mapPublic,{mapAbs,jsAbs,jsPublic,jsContent:content,existingDebugId:readDebugIdFromJs(content)})}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();if(!pair.existingDebugId){let injectedJs=injectDebugIdIntoJs(pair.jsContent,debugId);await writeFile(pair.jsAbs,injectedJs,`utf8`)}let finalMap=JSON.parse(mapContent).debugId===debugId?mapContent:injectDebugIdIntoMap(mapContent,debugId);return finalMap!==mapContent&&await writeFile(pair.mapAbs,finalMap,`utf8`),{absolute:pair.mapAbs,path:mapPublic,content:finalMap,hash:createHash(`sha256`).update(finalMap).digest(`hex`),debugId,chunkUrl:pair.jsPublic}}))).filter(f=>f!==null),sourceFileCount:pairs.size}}export{discoverWebpack,injectDebugIdIntoMap};
@@ -1,16 +1,10 @@
1
1
  import { NextBuildBundler } from "../detect-bundler.mjs";
2
- import { injectDebugIdIntoJs, injectDebugIdIntoMap } from "./discover-webpack.mjs";
2
+ import { injectDebugIdIntoMap } from "./discover-webpack.mjs";
3
+ import { injectDebugIdIntoJs } from "@interfere/types/data/source-maps";
3
4
 
4
5
  //#region src/internal/build/source-maps/discover.d.ts
5
6
  interface SourceMapFile {
6
7
  readonly absolute: string;
7
- /**
8
- * Public URL the runtime will load this chunk from. ALWAYS populated
9
- * for both bundlers — turbopack discovery walks JS chunks (in addition
10
- * to .map files) so it can record this. Lifted onto the manifest as
11
- * `chunkUrl`; server-side suffix-matching pairs `frame.fileName` to
12
- * this field by content-hashed filename.
13
- */
14
8
  readonly chunkUrl: string;
15
9
  readonly content: string;
16
10
  readonly debugId: string;
@@ -21,13 +15,6 @@ interface DiscoveryResult {
21
15
  readonly files: SourceMapFile[];
22
16
  readonly sourceFileCount: number;
23
17
  }
24
- /**
25
- * Single entry point for source-map discovery. The pipeline calls this
26
- * with the active bundler from `detectActiveBundler()`; new bundlers (e.g.
27
- * rspack) drop in here as additional cases without touching the pipeline.
28
- * Other manifest bundlers (for Vite, CLI, etc.) live outside the Next
29
- * build pipeline and should use their own discovery implementation.
30
- */
31
18
  declare function discoverSourceMaps(opts: {
32
19
  bundler: NextBuildBundler;
33
20
  projectDir: string;
@@ -1 +1 @@
1
- import{normalizeDistDir}from"./paths.mjs";import{discoverTurbopack}from"./discover-turbopack.mjs";import{discoverWebpack,injectDebugIdIntoJs,injectDebugIdIntoMap}from"./discover-webpack.mjs";async function discoverSourceMaps(opts){let distDir=normalizeDistDir(opts.distDir);switch(opts.bundler){case`webpack`:return discoverWebpack({projectDir:opts.projectDir,distDir});case`turbopack`:return discoverTurbopack({projectDir:opts.projectDir,distDir})}}export{discoverSourceMaps,injectDebugIdIntoJs,injectDebugIdIntoMap};
1
+ import{normalizeDistDir}from"./paths.mjs";import{discoverTurbopack}from"./discover-turbopack.mjs";import{discoverWebpack,injectDebugIdIntoMap}from"./discover-webpack.mjs";import{injectDebugIdIntoJs}from"@interfere/types/data/source-maps";async function discoverSourceMaps(opts){let distDir=normalizeDistDir(opts.distDir);switch(opts.bundler){case`webpack`:return discoverWebpack({projectDir:opts.projectDir,distDir});case`turbopack`:return discoverTurbopack({projectDir:opts.projectDir,distDir})}}export{discoverSourceMaps,injectDebugIdIntoJs,injectDebugIdIntoMap};
@@ -9,6 +9,6 @@ interface OnRequestErrorRequest {
9
9
  }
10
10
  declare function onRequestError(error: Error & {
11
11
  digest?: string;
12
- }, request: OnRequestErrorRequest | undefined, context: OnRequestErrorContext): void;
12
+ }, request: OnRequestErrorRequest | undefined, context: OnRequestErrorContext): Promise<void>;
13
13
  //#endregion
14
14
  export { captureError, onRequestError };
@@ -1 +1 @@
1
- import{isEnabledOnServer}from"../env.mjs";import{isPluginEnabled}from"./remote-config.mjs";import{readServerAction}from"./server-action-marker.mjs";import{SpanKind,SpanStatusCode,trace}from"@opentelemetry/api";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";const ON_REQUEST_ERROR_MECHANISM={type:MECHANISM_TYPE.nextjs.onRequestError,handled:!1,synthetic:!1},SERVER_ACTION_MECHANISM={type:MECHANISM_TYPE.nextjs.serverAction,handled:!1,synthetic:!1},DEFAULT_CAPTURE_ERROR_MECHANISM={type:MECHANISM_TYPE.nextjs.captureError,handled:!0},seen=new WeakSet;function buildAttrs(value,mechanism,context){let attrs={"interfere.exception.mechanism":mechanism.type,"interfere.exception.handled":mechanism.handled},digest=context?.nextjs?.errorDigest;if(digest&&(attrs[`interfere.error.digest`]=digest),context?.nextjs?.requestMethod&&(attrs[`http.request.method`]=context.nextjs.requestMethod),context?.nextjs?.requestPath&&(attrs[`url.path`]=context.nextjs.requestPath),context?.nextjs?.routePath&&(attrs[`http.route`]=context.nextjs.routePath),context?.nextjs?.routeType===`route`&&!mechanism.handled&&(attrs[`http.response.status_code`]=500),context?.serverAction&&(attrs[`interfere.server_action.name`]=context.serverAction.name,context.serverAction.argsShape&&(attrs[`interfere.server_action.args_shape`]=context.serverAction.argsShape)),context?.requestHeaders)for(let[name,headerValue]of Object.entries(context.requestHeaders))attrs[`http.request.header.${name}`]=headerValue;return isNonErrorException(value)?(attrs[`exception.type`]=value.type,attrs[`exception.message`]=value.value,attrs[`interfere.exception.kind`]=`non-error`,attrs):(attrs[`exception.type`]=value.name,attrs[`exception.message`]=value.message,attrs[`interfere.exception.kind`]=`error`,value.stack&&(attrs[`exception.stacktrace`]=value.stack),attrs)}function recordException({error,mechanism,context}){let value=toException(error),attrs=buildAttrs(value,mechanism,context),tracer=trace.getTracer(`@interfere/next/server`),active=trace.getActiveSpan();if(active?.isRecording()){active.addEvent(`exception`,attrs),mechanism.handled||active.setStatus({code:SpanStatusCode.ERROR,message:isNonErrorException(value)?value.value:value.message});return}let span=tracer.startSpan(`interfere.captureError`,{kind:SpanKind.INTERNAL});span.addEvent(`exception`,attrs),mechanism.handled||span.setStatus({code:SpanStatusCode.ERROR,message:isNonErrorException(value)?value.value:value.message}),span.end()}function captureError(error,_request,context){if(isEnabledOnServer()&&isPluginEnabled(`errors`)){if(error instanceof Error){if(seen.has(error))return;seen.add(error)}recordException({error,mechanism:context?.mechanism??DEFAULT_CAPTURE_ERROR_MECHANISM,context})}}const CAPTURED_REQUEST_HEADERS=[`accept`,`accept-language`,`content-length`,`content-type`,`host`,`referer`,`user-agent`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`,`x-vercel-id`];function pickAllowedHeaders(headers){if(!headers)return;let picked={};for(let name of CAPTURED_REQUEST_HEADERS){let value=headers[name];typeof value==`string`&&(picked[name]=value)}return Object.keys(picked).length>0?picked:void 0}function onRequestError(error,request,context){if(seen.has(error)||(seen.add(error),!isEnabledOnServer())||!isPluginEnabled(`errors`))return;let action=readServerAction(error),mechanism=action?SERVER_ACTION_MECHANISM:ON_REQUEST_ERROR_MECHANISM,requestHeaders=pickAllowedHeaders(request?.headers);recordException({error,mechanism,context:{mechanism,nextjs:{...context,...request?.method?{requestMethod:request.method}:{},...request?.path?{requestPath:request.path}:{},...error.digest?{errorDigest:error.digest}:{}},...requestHeaders?{requestHeaders}:{},...action?{serverAction:action}:{}}})}export{captureError,onRequestError};
1
+ import{isEnabledOnServer}from"../env.mjs";import{flushServerTelemetry}from"./server-flush.mjs";import{isPluginEnabled}from"./remote-config.mjs";import{readServerAction}from"./server-action-marker.mjs";import{SpanKind,SpanStatusCode,trace}from"@opentelemetry/api";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";const ON_REQUEST_ERROR_MECHANISM={type:MECHANISM_TYPE.nextjs.onRequestError,handled:!1,synthetic:!1},SERVER_ACTION_MECHANISM={type:MECHANISM_TYPE.nextjs.serverAction,handled:!1,synthetic:!1},ROUTE_HANDLER_MECHANISM={type:MECHANISM_TYPE.nextjs.routeHandler,handled:!1,synthetic:!1},DEFAULT_CAPTURE_ERROR_MECHANISM={type:MECHANISM_TYPE.nextjs.captureError,handled:!0},CONTROL_FLOW_DIGEST_PREFIXES=[`NEXT_REDIRECT`,`NEXT_NOT_FOUND`,`NEXT_HTTP_ERROR_FALLBACK`];function isNextControlFlowError(error){if(!(error instanceof Error))return!1;let digest=error.digest;return typeof digest==`string`&&CONTROL_FLOW_DIGEST_PREFIXES.some(prefix=>digest.startsWith(prefix))}const seen=new WeakSet;function buildAttrs(value,mechanism,context){let attrs={"interfere.exception.mechanism":mechanism.type,"interfere.exception.handled":mechanism.handled},digest=context?.nextjs?.errorDigest;if(digest&&(attrs[`interfere.error.digest`]=digest),context?.nextjs?.requestMethod&&(attrs[`http.request.method`]=context.nextjs.requestMethod),context?.nextjs?.requestPath&&(attrs[`url.path`]=context.nextjs.requestPath),context?.nextjs?.routePath&&(attrs[`http.route`]=context.nextjs.routePath),context?.nextjs?.routeType===`route`&&!mechanism.handled&&(attrs[`http.response.status_code`]=500),context?.serverAction&&(attrs[`interfere.server_action.name`]=context.serverAction.name,context.serverAction.argsShape&&(attrs[`interfere.server_action.args_shape`]=context.serverAction.argsShape)),context?.requestHeaders)for(let[name,headerValue]of Object.entries(context.requestHeaders))attrs[`http.request.header.${name}`]=headerValue;return isNonErrorException(value)?(attrs[`exception.type`]=value.type,attrs[`exception.message`]=value.value,attrs[`interfere.exception.kind`]=`non-error`,attrs):(attrs[`exception.type`]=value.name,attrs[`exception.message`]=value.message,attrs[`interfere.exception.kind`]=`error`,value.stack&&(attrs[`exception.stacktrace`]=value.stack),attrs)}function recordException({error,mechanism,context}){let value=toException(error),attrs=buildAttrs(value,mechanism,context),tracer=trace.getTracer(`@interfere/next/server`),active=trace.getActiveSpan();if(active?.isRecording()){active.addEvent(`exception`,attrs),mechanism.handled||active.setStatus({code:SpanStatusCode.ERROR,message:isNonErrorException(value)?value.value:value.message});return}let span=tracer.startSpan(`interfere.captureError`,{kind:SpanKind.INTERNAL});span.addEvent(`exception`,attrs),mechanism.handled||span.setStatus({code:SpanStatusCode.ERROR,message:isNonErrorException(value)?value.value:value.message}),span.end()}function captureError(error,_request,context){if(isEnabledOnServer()&&isPluginEnabled(`errors`)&&!isNextControlFlowError(error)){if(error instanceof Error){if(seen.has(error))return;seen.add(error)}recordException({error,mechanism:context?.mechanism??DEFAULT_CAPTURE_ERROR_MECHANISM,context})}}const CAPTURED_REQUEST_HEADERS=[`accept`,`accept-language`,`content-length`,`content-type`,`host`,`referer`,`user-agent`,`x-forwarded-for`,`x-forwarded-host`,`x-forwarded-proto`,`x-vercel-id`];function pickAllowedHeaders(headers){if(!headers)return;let picked={};for(let name of CAPTURED_REQUEST_HEADERS){let value=headers[name];typeof value==`string`&&(picked[name]=value)}return Object.keys(picked).length>0?picked:void 0}async function onRequestError(error,request,context){if(isNextControlFlowError(error)||seen.has(error)||(seen.add(error),!isEnabledOnServer())||!isPluginEnabled(`errors`))return;let action=readServerAction(error),mechanism=ON_REQUEST_ERROR_MECHANISM;action?mechanism=SERVER_ACTION_MECHANISM:context.routeType===`route`&&(mechanism=ROUTE_HANDLER_MECHANISM);let requestHeaders=pickAllowedHeaders(request?.headers);recordException({error,mechanism,context:{mechanism,nextjs:{...context,...request?.method?{requestMethod:request.method}:{},...request?.path?{requestPath:request.path}:{},...error.digest?{errorDigest:error.digest}:{}},...requestHeaders?{requestHeaders}:{},...action?{serverAction:action}:{}}});try{await flushServerTelemetry()}catch{return}}export{captureError,onRequestError};
@@ -0,0 +1,5 @@
1
+ //#region src/internal/server/server-flush.d.ts
2
+ declare function setServerFlush(fn: () => Promise<void>): void;
3
+ declare function flushServerTelemetry(): Promise<void>;
4
+ //#endregion
5
+ export { flushServerTelemetry, setServerFlush };
@@ -0,0 +1 @@
1
+ let flushFn=null;function setServerFlush(fn){flushFn=fn}function flushServerTelemetry(){return flushFn?flushFn():Promise.resolve()}export{flushServerTelemetry,setServerFlush};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/next`,version=`11.0.0-canary.5`;export{name,version};
1
+ var name=`@interfere/next`,version=`11.0.0-canary.7`;export{name,version};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/next",
3
- "version": "11.0.0-canary.5",
3
+ "version": "11.0.0-canary.7",
4
4
  "license": "MIT",
5
5
  "description": "Build software that never breaks.",
6
6
  "keywords": [
@@ -77,11 +77,6 @@
77
77
  "types": "./dist/provider.d.mts",
78
78
  "default": "./dist/provider.mjs"
79
79
  },
80
- "./route-handler": {
81
- "monorepo": "./src/route-handler.ts",
82
- "types": "./dist/route-handler.d.mts",
83
- "default": "./dist/route-handler.mjs"
84
- },
85
80
  "./server": {
86
81
  "monorepo": "./src/server.ts",
87
82
  "types": "./dist/server.d.mts",
@@ -98,11 +93,11 @@
98
93
  "typecheck": "tsgo --noEmit"
99
94
  },
100
95
  "dependencies": {
101
- "@interfere/constants": "^9.0.2-canary.0",
102
- "@interfere/helpers": "^9.0.0",
103
- "@interfere/react": "^11.0.0-canary.4",
104
- "@interfere/sdk": "^11.0.0-canary.4",
105
- "@interfere/types": "^9.0.3-canary.2",
96
+ "@interfere/constants": "^9.0.2-canary.3",
97
+ "@interfere/helpers": "^9.0.1-canary.2",
98
+ "@interfere/react": "^11.0.0-canary.7",
99
+ "@interfere/sdk": "^11.0.0-canary.7",
100
+ "@interfere/types": "^10.0.0-canary.6",
106
101
  "@opentelemetry/api": "^1.9.1",
107
102
  "@opentelemetry/api-logs": "^0.219.0",
108
103
  "@opentelemetry/baggage-span-processor": "^0.5.0",
@@ -138,12 +133,12 @@
138
133
  }
139
134
  },
140
135
  "devDependencies": {
141
- "@interfere/test-utils": "^9.0.0",
142
- "@interfere/typescript-config": "^9.0.0",
143
- "@types/node": "^26.0.0",
136
+ "@interfere/test-utils": "^9.0.1-canary.1",
137
+ "@interfere/typescript-config": "^9.0.1-canary.1",
138
+ "@types/node": "^26.1.0",
144
139
  "@types/react": "19.2.17",
145
140
  "@types/react-dom": "19.2.3",
146
- "@typescript/native-preview": "7.0.0-dev.20260623.1",
141
+ "@typescript/native-preview": "7.0.0-dev.20260701.1",
147
142
  "@vitest/browser": "4.1.9",
148
143
  "@vitest/browser-playwright": "4.1.9",
149
144
  "@vitest/coverage-v8": "4.1.9",
@@ -1,17 +0,0 @@
1
- //#region src/internal/route/handle-get.d.ts
2
- /**
3
- * Generic GET proxy. SDK 10.x routes `GET /v2/config` (and any future
4
- * GET-based endpoints) through here; SDK 9.x clients still hit
5
- * `GET /v1/config` and get the same delegated response on the collector
6
- * side.
7
- *
8
- * `/sw` is special-cased to serve the SDK's bundled service-worker
9
- * script directly out of the customer's Next.js process. `SW_SCRIPT`
10
- * is a string export emitted at @interfere/react build time
11
- * (`scripts/build-sw.ts`); on Node runtime the customer resolves it
12
- * via `node_modules`, on Edge runtime the customer's Next.js bundler
13
- * inlines it into the Edge worker. Either way, no `fs.readFileSync`.
14
- */
15
- declare function handleGet(request: Request): Promise<Response>;
16
- //#endregion
17
- export { handleGet };
@@ -1 +0,0 @@
1
- import{log}from"../logger.mjs";import{extractSubPath,formatProxyError,forwardToCollector,hasPublicKeyCredential,notConfiguredResponse,resolveAuthenticatedEnv}from"./proxy.mjs";import{SW_SCRIPT}from"@interfere/react/sw";const SW_HEADERS={"content-type":`application/javascript; charset=utf-8`,"service-worker-allowed":`/`,"cache-control":`public, max-age=3600`};async function handleGet(request){let subPath=extractSubPath(request);if(subPath===`/sw`)return new Response(SW_SCRIPT,{status:200,headers:SW_HEADERS});let env=resolveAuthenticatedEnv();if(!hasPublicKeyCredential(request,env.publicKey))return notConfiguredResponse();try{return await forwardToCollector(request,env,subPath)}catch(error){let detail=formatProxyError(error);return log.error(`Proxy ${request.method} ${subPath} failed`,detail.lines),Response.json({code:`INTERFERE_PROXY_ERROR`,message:detail.message},{status:502})}}export{handleGet};
@@ -1,15 +0,0 @@
1
- //#region src/internal/route/handle-post.d.ts
2
- /**
3
- * Generic POST proxy. SDK 10.x routes everything through here:
4
- *
5
- * - `/v2/sink` (OTLP traces / metrics / logs)
6
- * - `/v2/replay/upload/:sessionId`
7
- * - `/v1/session*` (session sync + identify)
8
- *
9
- * No envelope-aware special-casing — that endpoint (`/v1/ingest`) was
10
- * 9.x-only and is not produced by 10.x. Customers on 9.x post directly
11
- * to the collector through the same proxy without touching this file.
12
- */
13
- declare function handlePost(request: Request): Promise<Response>;
14
- //#endregion
15
- export { handlePost };
@@ -1 +0,0 @@
1
- import{log}from"../logger.mjs";import{extractSubPath,formatProxyError,forwardToCollector,hasPublicKeyCredential,notConfiguredResponse,resolveAuthenticatedEnv}from"./proxy.mjs";async function handlePost(request){let env=resolveAuthenticatedEnv();if(!hasPublicKeyCredential(request,env.publicKey))return notConfiguredResponse();let subPath=extractSubPath(request);try{return await forwardToCollector(request,env,subPath)}catch(error){let detail=formatProxyError(error);return log.error(`Proxy ${request.method} ${subPath} failed`,detail.lines),Response.json({code:`INTERFERE_PROXY_ERROR`,message:detail.message},{status:502})}}export{handlePost};
@@ -1,20 +0,0 @@
1
- import { InterfereEnv } from "../env.mjs";
2
-
3
- //#region src/internal/route/proxy.d.ts
4
- interface AuthenticatedEnv {
5
- apiUrl: string;
6
- publicKey: string | null;
7
- release: InterfereEnv["release"];
8
- }
9
- declare function resolveAuthenticatedEnv(): AuthenticatedEnv;
10
- /** True when the proxy can attach a publishable key from env or the request. */
11
- declare function hasPublicKeyCredential(request: Request, envPublicKey: string | null): boolean;
12
- declare function notConfiguredResponse(): Response;
13
- declare function extractSubPath(request: Request): string;
14
- declare function formatProxyError(error: unknown): {
15
- message: string;
16
- lines: string[];
17
- };
18
- declare function forwardToCollector(request: Request, env: AuthenticatedEnv, subPath: string): Promise<Response>;
19
- //#endregion
20
- export { AuthenticatedEnv, extractSubPath, formatProxyError, forwardToCollector, hasPublicKeyCredential, notConfiguredResponse, resolveAuthenticatedEnv };
@@ -1 +0,0 @@
1
- import{readInterfereEnv}from"../env.mjs";import{log}from"../logger.mjs";import{omitUndefined}from"@interfere/helpers/omit-undefined";import{resolveRoutePrefix}from"@interfere/constants/route-prefix";import{extractCountryHeader}from"@interfere/types/sdk/geo";const STRIPPED_AUTH_HEADERS=[`authorization`,`x-api-key`,`x-interfere-api-key`],STRIPPED_TRANSPORT_HEADERS=[`host`,`content-length`,`connection`,`keep-alive`,`transfer-encoding`,`te`,`trailer`,`upgrade`,`proxy-authenticate`,`proxy-authorization`];function forwardedHeaders(request){let headers=new Headers(request.headers);for(let name of STRIPPED_AUTH_HEADERS)headers.delete(name);for(let name of STRIPPED_TRANSPORT_HEADERS)headers.delete(name);return headers}function resolveAuthenticatedEnv(){let env=readInterfereEnv();return{apiUrl:env.apiUrl,publicKey:env.publicKey,release:env.release}}function hasPublicKeyCredential(request,envPublicKey){return envPublicKey?!0:new URL(request.url).searchParams.has(`pk`)}function notConfiguredResponse(){return log.warn(`Not configured`,[`INTERFERE_PUBLIC_KEY is not set and the request has no ?pk= query param. The proxy route will return 503.`]),Response.json({code:`INTERFERE_NOT_CONFIGURED`,message:`INTERFERE_PUBLIC_KEY is required.`},{status:503})}function extractSubPath(request){let prefix=resolveRoutePrefix(),url=new URL(request.url),idx=url.pathname.indexOf(prefix);if(idx===-1)return`/`;let remainder=url.pathname.slice(idx+prefix.length);return remainder===``?`/`:remainder.startsWith(`/`)?remainder:`/${remainder}`}function formatProxyError(error){if(!(error instanceof Error))return{message:String(error),lines:[String(error)]};let lines=[`${error.name}: ${error.message}`];if(`cause`in error&&error.cause){let cause=error.cause instanceof Error?error.cause.message:String(error.cause);lines.push(`cause: ${cause}`)}return`code`in error&&typeof error.code==`string`&&lines.push(`code: ${error.code}`),{message:`${error.name}: ${error.message}`,lines}}function buildUpstreamUrl(apiUrl,subPath,request,publicKey){let incoming=new URL(request.url),upstream=new URL(`${apiUrl}${subPath}`);for(let[key,value]of incoming.searchParams)upstream.searchParams.append(key,value);return publicKey&&!upstream.searchParams.has(`pk`)&&upstream.searchParams.set(`pk`,publicKey),upstream.toString()}async function forwardToCollector(request,env,subPath){let url=buildUpstreamUrl(env.apiUrl,subPath,request,env.publicKey),body=request.method!==`GET`&&request.method!==`HEAD`?new Uint8Array(await request.arrayBuffer()):void 0,headers=forwardedHeaders(request);headers.has(`content-type`)||headers.set(`content-type`,`application/json`);let country=extractCountryHeader(request);country!==null&&headers.set(`x-country-code`,country);let upstream=await fetch(url,{...omitUndefined({body}),method:request.method,headers,signal:AbortSignal.timeout(1e4)});if(!upstream.ok){let text=await upstream.text().catch(()=>``);return upstream.status>=500?(log.error(`Upstream ${upstream.status} for ${request.method} ${subPath}`,[text]),Response.json({code:`INTERFERE_UPSTREAM_ERROR`,message:text},{status:upstream.status})):upstream.status>=400?(log.warn(`Upstream ${upstream.status} for ${request.method} ${subPath}`,[text]),Response.json({code:`INTERFERE_UPSTREAM_WARNING`,message:text},{status:upstream.status})):(log.error(`Upstream ${upstream.status} for ${request.method} ${subPath}`,[text]),Response.json({code:`INTERFERE_UPSTREAM_ERROR`,message:text},{status:upstream.status}))}return new Response(upstream.body,{status:upstream.status,headers:{"content-type":upstream.headers.get(`content-type`)??`application/json`}})}export{extractSubPath,formatProxyError,forwardToCollector,hasPublicKeyCredential,notConfiguredResponse,resolveAuthenticatedEnv};
@@ -1,12 +0,0 @@
1
- //#region src/route-handler.d.ts
2
- /**
3
- * Customers consume this module via `export * from "@interfere/next/route-handler"`
4
- * so any HTTP method we add later (PATCH, DELETE, …) is picked up
5
- * without a customer file change. Do NOT add non-method exports here —
6
- * `route-handler.lint.test.ts` will fail the build if you do.
7
- */
8
- declare function GET(request: Request): Promise<Response>;
9
- declare function POST(request: Request): Promise<Response>;
10
- declare function OPTIONS(): Response;
11
- //#endregion
12
- export { GET, OPTIONS, POST };
@@ -1 +0,0 @@
1
- import{isEnabledOnServer}from"./internal/env.mjs";import{handleGet}from"./internal/route/handle-get.mjs";import{handlePost}from"./internal/route/handle-post.mjs";const DISABLED=()=>new Response(null,{status:204});async function GET(request){return isEnabledOnServer()?await handleGet(request):DISABLED()}function POST(request){return isEnabledOnServer()?handlePost(request):Promise.resolve(DISABLED())}const CORS_HEADERS={"Access-Control-Allow-Origin":`*`,"Access-Control-Allow-Methods":`GET, POST, OPTIONS`,"Access-Control-Allow-Headers":`Content-Type`,"Access-Control-Max-Age":`86400`};function OPTIONS(){return new Response(null,{status:200,headers:CORS_HEADERS})}export{GET,OPTIONS,POST};