@interfere/vite 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/vite</h1>
@@ -19,15 +19,10 @@
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>
30
-
31
26
  ---
32
27
 
33
28
  ## Getting Started
@@ -91,17 +86,18 @@ That's it. The provider auto-resolves the kernel created by `init()`; you don't
91
86
 
92
87
  ### Quick Start (TanStack Start + Nitro)
93
88
 
94
- TanStack Start runs through Nitro, so the SDK mirrors the Next.js shape: browser events go to a same-origin proxy route, and the server forwards the SDK's public-key-authenticated requests.
95
-
96
- Use the same browser imports — call `init()` from the file that boots your router, and put `<InterfereProvider>` in `__root.tsx`:
89
+ Browser events post directly to Interfere; the server's own telemetry is registered via `@interfere/vite/server`. Call `init()` (browser) and `register()` (server) from the file that boots your router, and put `<InterfereProvider>` in `__root.tsx`:
97
90
 
98
91
  ```ts
99
92
  // src/router.tsx
100
93
  import { init } from "@interfere/vite/init";
94
+ import { register } from "@interfere/vite/server";
101
95
  import { createRouter } from "@tanstack/react-router";
102
96
  import { routeTree } from "./routeTree.gen";
103
97
 
104
- if (typeof window !== "undefined") {
98
+ if (typeof window === "undefined") {
99
+ register().catch(() => {});
100
+ } else {
105
101
  init();
106
102
  }
107
103
 
@@ -123,33 +119,7 @@ function RootComponent() {
123
119
  }
124
120
  ```
125
121
 
126
- The browser guard matters for TanStack Start because prerender can evaluate the router import graph on the server, while the Interfere kernel uses browser-only APIs.
127
-
128
- Then mount the proxy handler and server instrumentation in a catch-all route:
129
-
130
- ```tsx
131
- // src/routes/api/interfere/$.tsx
132
- import { handle } from "@interfere/vite/handler";
133
- import { register } from "@interfere/vite/server";
134
-
135
- import { createFileRoute } from "@tanstack/react-router";
136
-
137
- if (typeof window === "undefined") {
138
- register().catch((error: unknown) => {
139
- console.warn("[interfere] register() threw during boot", error);
140
- });
141
- }
142
-
143
- export const Route = createFileRoute("/api/interfere/$")({
144
- server: {
145
- handlers: {
146
- GET: ({ request }) => handle(request),
147
- POST: ({ request }) => handle(request),
148
- OPTIONS: ({ request }) => handle(request),
149
- },
150
- },
151
- });
152
- ```
122
+ The browser guard matters for TanStack Start because prerender can evaluate the router import graph on the server, while the Interfere kernel uses browser-only APIs. For an ad-blocker-resistant setup, set `interfere({ apiHost: "https://in.yourapp.com" })` — a subdomain of your own site that resolves to Interfere via a DNS record.
153
123
 
154
124
  ## First-Time Setup
155
125
 
@@ -167,7 +137,7 @@ For **Vercel preview / production**, set `INTERFERE_API_KEY` in your `.env.local
167
137
 
168
138
  | Variable | Required | Description |
169
139
  | --- | --- | --- |
170
- | `VITE_INTERFERE_PUBLIC_KEY` | Yes | Surface publishable key in the `interfere_pub_us_*` or `interfere_pub_eu_*` format. Stamped into the page by the plugin at build time for both direct and proxy mode. |
140
+ | `VITE_INTERFERE_PUBLIC_KEY` | Yes | Surface publishable key in the `interfere_pub_us_*` or `interfere_pub_eu_*` format. Stamped into the page by the plugin at build time, and reused server-side (`register()`, request middleware) — the plugin mirrors it into the dev server's process, so no separate server variable is needed. |
171
141
  | `INTERFERE_API_KEY` | Yes for release metadata | Interfere secret key in the `interfere_secret_us_*` or `interfere_secret_eu_*` format. Used by the plugin at build time. Without it, release metadata is not published and the collector rejects events as `COLLECTOR_RELEASE_PREFLIGHT_UNCONFIRMED`. |
172
142
  | `INTERFERE_API_URL` | No | Override the collector URL the build pipeline targets. Defaults to `https://in.interfere.com`. |
173
143
  | `VITE_INTERFERE_API_URL` | No | Override the collector URL the browser kernel targets in direct mode. Proxy mode uses `INTERFERE_API_URL` server-side. |
@@ -184,10 +154,12 @@ interfere({
184
154
 
185
155
  | Option | Default | Description |
186
156
  | --- | --- | --- |
157
+ | `apiHost` | — | The endpoint the browser SDK posts to. Defaults to Interfere's endpoint. Set it to a subdomain of your own site (e.g. `"https://in.yourapp.com"`) that resolves to Interfere via a DNS record to survive ad-blockers, or to a regional endpoint. Add a CNAME pointing at the target shown in your dashboard; Interfere provisions TLS automatically. |
187
158
  | `commitSha` | `VITE_INTERFERE_COMMIT_SHA` → `git rev-parse HEAD` | Commit SHA used to derive the deterministic release slug. |
188
- | `mode` | `"auto"` | `"auto"` detects TanStack Start / Nitro and uses proxy mode; vanilla Vite uses direct mode. Override with `"direct"` or `"proxy"` when needed. |
189
159
  | `sourceMaps` | auto | `false` disables build-time release metadata publishing. By default the pipeline runs when `INTERFERE_API_KEY` and a commit SHA are available. |
190
160
 
161
+ Ingest requests never carry cookies. `apiHost` posts to your own site's subdomain, surviving ad-blockers with no server-side component.
162
+
191
163
  ## Migrating From 0.x
192
164
 
193
165
  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.
package/dist/init.d.mts CHANGED
@@ -1,15 +1,16 @@
1
1
  import { capture, span } from "@interfere/react/api";
2
- import { Kernel, KernelOptions } from "@interfere/react/internal/kernel";
2
+ import { Kernel, Kernel as Kernel$1, KernelOptions, KernelOptions as KernelOptions$1 } from "@interfere/react/internal/kernel";
3
3
  import { ConsentCategory, ConsentState, GateableCategory } from "@interfere/types/sdk/plugins/manifest";
4
4
 
5
5
  //#region src/init.d.ts
6
+ declare function init(opts?: KernelOptions$1): Promise<Kernel$1 | null>;
6
7
  declare const close: () => Promise<void>, consent: {
7
8
  get(): import("@interfere/types/sdk/plugins/manifest").ConsentState | null;
8
9
  set(value?: import("@interfere/types/sdk/plugins/manifest").ConsentState): void;
9
- }, getKernel: () => import("@interfere/react/internal/kernel").Kernel, getKernelOrNull: () => import("@interfere/react/internal/kernel").Kernel | null, identity: {
10
+ }, getKernel: () => Kernel$1, getKernelOrNull: () => Kernel$1 | null, identity: {
10
11
  clear(): void;
11
12
  get(): import("@interfere/types/sdk/identify").IdentifyParams | null;
12
13
  set(params: import("@interfere/types/sdk/identify").IdentifyParams): Promise<void>;
13
- }, init: (opts?: import("@interfere/react/internal/kernel").KernelOptions) => Promise<import("@interfere/react/internal/kernel").Kernel | null>, subscribeToKernel: (listener: () => void) => () => void;
14
+ }, subscribeToKernel: (listener: () => void) => () => void;
14
15
  //#endregion
15
16
  export { type ConsentCategory, type ConsentState, type GateableCategory, type Kernel, type KernelOptions, capture, close, consent, getKernel, getKernelOrNull, identity, init, span, subscribeToKernel };
package/dist/init.mjs CHANGED
@@ -1 +1 @@
1
- import{PRODUCER_VERSION}from"./version.mjs";import{createWrapperSingleton}from"@interfere/react/internal/wrapper-singleton";import{capture,span}from"@interfere/react/api";const{close,consent,getKernel,getKernelOrNull,identity,init,subscribeToKernel}=createWrapperSingleton({producerVersion:PRODUCER_VERSION,initEntryName:`Vite`});export{capture,close,consent,getKernel,getKernelOrNull,identity,init,span,subscribeToKernel};
1
+ import{applyBrowserGlobals}from"./internal/browser-globals.mjs";import{PRODUCER_VERSION}from"./version.mjs";import{createWrapperSingleton}from"@interfere/react/internal/wrapper-singleton";import{capture,span}from"@interfere/react/api";const wrapper=createWrapperSingleton({producerVersion:PRODUCER_VERSION,initEntryName:`Vite`});function init(opts){return applyBrowserGlobals(),wrapper.init(opts)}const{close,consent,getKernel,getKernelOrNull,identity,subscribeToKernel}=wrapper;export{capture,close,consent,getKernel,getKernelOrNull,identity,init,span,subscribeToKernel};
@@ -0,0 +1,4 @@
1
+ //#region src/internal/browser-globals.d.ts
2
+ declare function applyBrowserGlobals(): void;
3
+ //#endregion
4
+ export { applyBrowserGlobals };
@@ -0,0 +1 @@
1
+ function applyBrowserGlobals(){let g=globalThis;import.meta.env.VITE_INTERFERE_PUBLIC_KEY&&(g.__INTERFERE_PUBLIC_KEY__??=import.meta.env.VITE_INTERFERE_PUBLIC_KEY),import.meta.env.VITE_INTERFERE_API_HOST&&(g.__INTERFERE_API_URL__??=import.meta.env.VITE_INTERFERE_API_HOST),import.meta.env.VITE_INTERFERE_ENVIRONMENT&&(g.__INTERFERE_ENVIRONMENT__??=import.meta.env.VITE_INTERFERE_ENVIRONMENT),import.meta.env.VITE_INTERFERE_RELEASE_SLUG&&(g.__INTERFERE_RELEASE_SLUG__??=import.meta.env.VITE_INTERFERE_RELEASE_SLUG);let forceEnable=import.meta.env.VITE_INTERFERE_FORCE_ENABLE;(forceEnable===`1`||forceEnable===`true`)&&(g.__INTERFERE_FORCE_ENABLE__??=!0)}export{applyBrowserGlobals};
@@ -1,8 +1,8 @@
1
1
  import { DiscoveryResult } from "./source-maps.mjs";
2
2
  import { ReleaseSlug } from "@interfere/types/releases/slug";
3
3
  import { HTTPClient, Interfere } from "@interfere/sdk";
4
- import { ReleasesConfigResponse } from "@interfere/sdk/models/releases-config-response.js";
5
4
  import { ManifestBundler } from "@interfere/types/data/source-maps";
5
+ import { ReleasesConfigResponse } from "@interfere/sdk/models/releases-config-response.js";
6
6
  import { CreateReleaseResponse } from "@interfere/types/releases/definition";
7
7
 
8
8
  //#region src/internal/build/pipeline.d.ts
@@ -1 +1 @@
1
- import{cleanupSourceMaps,uploadSourceMaps}from"./source-maps.mjs";import{parseEnvValue}from"@interfere/types/sdk/env";import{releaseSlugSchema}from"@interfere/types/releases/slug";import{applyDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{execSync}from"node:child_process";import{HTTPClient,Interfere}from"@interfere/sdk";import{releaseDestinationProviderSchema}from"@interfere/types/integrations";import{resolveCliReleaseDestination}from"@interfere/types/sdk/cli-destination";const sources={github:resolveGithubSource},destinations={vercel:resolveVercelDestination,cloudflare:resolveCloudflareDestination,cli:resolveCliDestination};async function timed(fn){let start=performance.now();return[await fn(),Math.round(performance.now()-start)]}function createSdkClient(opts){let httpClient=new HTTPClient;return httpClient.addHook(`beforeRequest`,request=>{let next=new Request(request);return next.headers.set(`Authorization`,`Bearer ${opts.apiKey}`),next}),{httpClient,sdk:new Interfere({serverURL:opts.apiUrl,httpClient}),security:{apiKey:opts.apiKey}}}function resolveReleaseRequest(buildId,environment,releaseSlug,producerVersion,config){if(!config.surface.sourceProvider)return{ok:!1,reason:`missing_source_provider`};let destinationProvider=releaseDestinationProviderSchema.safeParse(config.surface.destinationProvider);return destinationProvider.success?{ok:!0,request:{source:sources[config.surface.sourceProvider](),destination:applyDeployEnvironment(destinations[destinationProvider.data](releaseSlug),environment),buildId,slug:releaseSlug,producerVersion}}:{ok:!1,reason:`missing_destination_provider`}}async function runReleasePipeline(input){let{apiKey,apiUrl,bundler,discovery,releasesConfig}=input,releaseRequest=resolveReleaseRequest(input.buildId,input.environment,input.releaseSlug,input.producerVersion,releasesConfig);if(!releaseRequest.ok)return{ok:!1,failure:{kind:releaseRequest.reason,config:releasesConfig}};let start=performance.now(),{httpClient,sdk,security}=createSdkClient({apiKey,apiUrl}),[release,createReleaseMs]=await timed(()=>sdk.releases.create(security,releaseRequest.request)),releaseSlug=releaseSlugSchema.parse(release.destination.slug),buildId=release.build.hash??input.buildId,[{totalBytes},uploadMs]=await timed(()=>uploadSourceMaps({apiUrl,bundler,discovered:discovery,httpClient,releaseSlug})),[,preflightMs]=await timed(()=>sdk.releases.preflight(security,{releaseSlug})),[{failures},cleanupMs]=await timed(()=>cleanupSourceMaps(discovery.files));return{ok:!0,data:{buildId,cleanupFailures:failures,config:releasesConfig,fileCount:discovery.files.length,release:{...release,destination:{...release.destination,slug:releaseSlug}},releaseSlug,timing:{createRelease:createReleaseMs,upload:uploadMs,preflight:preflightMs,cleanup:cleanupMs,total:Math.round(performance.now()-start),fileCount:discovery.files.length,totalBytes}}}}function runGitCommand(command){try{let output=execSync(command,{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return output.length>0?output:null}catch{return null}}function resolveGithubSource(){return{provider:`github`,branch:parseEnvValue(process.env.VERCEL_GIT_COMMIT_REF)??parseEnvValue(process.env.GITHUB_REF_NAME)??parseEnvValue(process.env.GITHUB_HEAD_REF)??runGitCommand(`git rev-parse --abbrev-ref HEAD`)??`unknown`,commitMessage:parseEnvValue(process.env.VERCEL_GIT_COMMIT_MESSAGE)??runGitCommand(`git log -1 --pretty=%B`)??``,commitSha:parseEnvValue(process.env.VERCEL_GIT_COMMIT_SHA)??parseEnvValue(process.env.GITHUB_SHA)??runGitCommand(`git rev-parse HEAD`)}}function resolveVercelDestination(){let environment=parseEnvValue(process.env.VERCEL_ENV??process.env.VERCEL_TARGET_ENV),deploymentId=parseEnvValue(process.env.VERCEL_DEPLOYMENT_ID);return{provider:`vercel`,destinationReleaseId:deploymentId,environment,deploymentId,deploymentUrl:resolveDeploymentUrl(),environmentName:environment,environmentTarget:environment}}function resolveDeploymentUrl(){let deploymentUrl=parseEnvValue(process.env.VERCEL_URL);return deploymentUrl===null?null:deploymentUrl.startsWith(`https://`)||deploymentUrl.startsWith(`http://`)?deploymentUrl:`https://${deploymentUrl}`}function resolveCloudflareDestination(){let raw=parseEnvValue(process.env.CF_VERSION_METADATA),versionMetadata=null;if(raw)try{versionMetadata=JSON.parse(raw)}catch{versionMetadata=null}let versionId=parseEnvValue(process.env.CF_VERSION_ID)??versionMetadata?.id??parseEnvValue(process.env.WORKERS_CI_BUILD_UUID)??null,scriptName=parseEnvValue(process.env.CF_WORKER_NAME)??parseEnvValue(process.env.WORKERS_CI_SCRIPT_NAME)??null,accountId=parseEnvValue(process.env.CF_ACCOUNT_ID)??parseEnvValue(process.env.CLOUDFLARE_ACCOUNT_ID)??null,environment=parseEnvValue(process.env.WORKERS_CI_BRANCH)??versionMetadata?.tag??null;return{provider:`cloudflare`,destinationReleaseId:versionId,environment,versionId,scriptName,accountId,deploymentUrl:resolveCloudflareDeploymentUrl(),environmentName:environment,environmentTarget:environment}}function resolveCloudflareDeploymentUrl(){let candidate=parseEnvValue(process.env.CF_PAGES_URL)??parseEnvValue(process.env.WORKERS_CI_DEPLOY_URL);return candidate===null?null:candidate.startsWith(`https://`)||candidate.startsWith(`http://`)?candidate:`https://${candidate}`}function resolveCliDestination(releaseSlug){return resolveCliReleaseDestination(process.env,releaseSlug)}export{createSdkClient,runReleasePipeline};
1
+ import{cleanupSourceMaps,uploadSourceMaps}from"./source-maps.mjs";import{releaseSlugSchema}from"@interfere/types/releases/slug";import{applyDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{execSync}from"node:child_process";import{HTTPClient,Interfere}from"@interfere/sdk";import{releaseDestinationProviderSchema}from"@interfere/types/integrations";import{resolveCliReleaseDestination}from"@interfere/types/sdk/cli-destination";import{parseEnvValue}from"@interfere/types/sdk/env";const sources={github:resolveGithubSource},destinations={vercel:resolveVercelDestination,cloudflare:resolveCloudflareDestination,cli:resolveCliDestination};async function timed(fn){let start=performance.now();return[await fn(),Math.round(performance.now()-start)]}function createSdkClient(opts){let httpClient=new HTTPClient;return httpClient.addHook(`beforeRequest`,request=>{let next=new Request(request);return next.headers.set(`Authorization`,`Bearer ${opts.apiKey}`),next}),{httpClient,sdk:new Interfere({serverURL:opts.apiUrl,httpClient}),security:{apiKey:opts.apiKey}}}function resolveReleaseRequest(buildId,environment,releaseSlug,producerVersion,config){if(!config.surface.sourceProvider)return{ok:!1,reason:`missing_source_provider`};let destinationProvider=releaseDestinationProviderSchema.safeParse(config.surface.destinationProvider);return destinationProvider.success?{ok:!0,request:{source:sources[config.surface.sourceProvider](),destination:applyDeployEnvironment(destinations[destinationProvider.data](releaseSlug),environment),buildId,slug:releaseSlug,producerVersion}}:{ok:!1,reason:`missing_destination_provider`}}async function runReleasePipeline(input){let{apiKey,apiUrl,bundler,discovery,releasesConfig}=input,releaseRequest=resolveReleaseRequest(input.buildId,input.environment,input.releaseSlug,input.producerVersion,releasesConfig);if(!releaseRequest.ok)return{ok:!1,failure:{kind:releaseRequest.reason,config:releasesConfig}};let start=performance.now(),{httpClient,sdk,security}=createSdkClient({apiKey,apiUrl}),[release,createReleaseMs]=await timed(()=>sdk.releases.create(security,releaseRequest.request)),releaseSlug=releaseSlugSchema.parse(release.destination.slug),buildId=release.build.hash??input.buildId,[{totalBytes},uploadMs]=await timed(()=>uploadSourceMaps({apiUrl,bundler,discovered:discovery,httpClient,releaseSlug})),[,preflightMs]=await timed(()=>sdk.releases.preflight(security,{releaseSlug})),[{failures},cleanupMs]=await timed(()=>cleanupSourceMaps(discovery.files));return{ok:!0,data:{buildId,cleanupFailures:failures,config:releasesConfig,fileCount:discovery.files.length,release:{...release,destination:{...release.destination,slug:releaseSlug}},releaseSlug,timing:{createRelease:createReleaseMs,upload:uploadMs,preflight:preflightMs,cleanup:cleanupMs,total:Math.round(performance.now()-start),fileCount:discovery.files.length,totalBytes}}}}function runGitCommand(command){try{let output=execSync(command,{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return output.length>0?output:null}catch{return null}}function resolveGithubSource(){return{provider:`github`,branch:parseEnvValue(process.env.VERCEL_GIT_COMMIT_REF)??parseEnvValue(process.env.GITHUB_REF_NAME)??parseEnvValue(process.env.GITHUB_HEAD_REF)??runGitCommand(`git rev-parse --abbrev-ref HEAD`)??`unknown`,commitMessage:parseEnvValue(process.env.VERCEL_GIT_COMMIT_MESSAGE)??runGitCommand(`git log -1 --pretty=%B`)??``,commitSha:parseEnvValue(process.env.VERCEL_GIT_COMMIT_SHA)??parseEnvValue(process.env.GITHUB_SHA)??runGitCommand(`git rev-parse HEAD`)}}function resolveVercelDestination(){let environment=parseEnvValue(process.env.VERCEL_ENV??process.env.VERCEL_TARGET_ENV),deploymentId=parseEnvValue(process.env.VERCEL_DEPLOYMENT_ID);return{provider:`vercel`,destinationReleaseId:deploymentId,environment,deploymentId,deploymentUrl:resolveDeploymentUrl(),environmentName:environment,environmentTarget:environment}}function resolveDeploymentUrl(){let deploymentUrl=parseEnvValue(process.env.VERCEL_URL);return deploymentUrl===null?null:deploymentUrl.startsWith(`https://`)||deploymentUrl.startsWith(`http://`)?deploymentUrl:`https://${deploymentUrl}`}function resolveCloudflareDestination(){let raw=parseEnvValue(process.env.CF_VERSION_METADATA),versionMetadata=null;if(raw)try{versionMetadata=JSON.parse(raw)}catch{versionMetadata=null}let versionId=parseEnvValue(process.env.CF_VERSION_ID)??versionMetadata?.id??parseEnvValue(process.env.WORKERS_CI_BUILD_UUID)??null,scriptName=parseEnvValue(process.env.CF_WORKER_NAME)??parseEnvValue(process.env.WORKERS_CI_SCRIPT_NAME)??null,accountId=parseEnvValue(process.env.CF_ACCOUNT_ID)??parseEnvValue(process.env.CLOUDFLARE_ACCOUNT_ID)??null,environment=parseEnvValue(process.env.WORKERS_CI_BRANCH)??versionMetadata?.tag??null;return{provider:`cloudflare`,destinationReleaseId:versionId,environment,versionId,scriptName,accountId,deploymentUrl:resolveCloudflareDeploymentUrl(),environmentName:environment,environmentTarget:environment}}function resolveCloudflareDeploymentUrl(){let candidate=parseEnvValue(process.env.CF_PAGES_URL)??parseEnvValue(process.env.WORKERS_CI_DEPLOY_URL);return candidate===null?null:candidate.startsWith(`https://`)||candidate.startsWith(`http://`)?candidate:`https://${candidate}`}function resolveCliDestination(releaseSlug){return resolveCliReleaseDestination(process.env,releaseSlug)}export{createSdkClient,runReleasePipeline};
@@ -1,6 +1,6 @@
1
1
  import { ReleaseSlug } from "@interfere/types/releases/slug";
2
2
  import { HTTPClient } from "@interfere/sdk";
3
- import { ManifestBundler } from "@interfere/types/data/source-maps";
3
+ import { ManifestBundler, injectDebugIdIntoJs } from "@interfere/types/data/source-maps";
4
4
 
5
5
  //#region src/internal/build/source-maps.d.ts
6
6
  interface SourceMapFile {
@@ -53,4 +53,4 @@ declare function uploadSourceMaps({
53
53
  releaseSlug
54
54
  }: UploadParams): Promise<UploadResult>;
55
55
  //#endregion
56
- export { CleanupFailure, DiscoverByPairingOptions, DiscoveryResult, SourceMapFile, UploadResult, cleanupSourceMaps, discoverByPairing, uploadSourceMaps };
56
+ export { CleanupFailure, DiscoverByPairingOptions, DiscoveryResult, SourceMapFile, UploadResult, cleanupSourceMaps, discoverByPairing, injectDebugIdIntoJs, uploadSourceMaps };
@@ -1 +1 @@
1
- import{isAbsolute,join,relative,resolve}from"node:path";import{createHash,randomUUID}from"node:crypto";import{readFile,readdir,unlink,writeFile}from"node:fs/promises";const LEADING_DOT_SLASH_RE=/^(\.\/)+/,SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/,SPEC_LAST_DEBUG_ID_RE=/\/\/# debugId=([a-fA-F0-9-]+)(?![\s\S]*\/\/# debugId=)/m,SCRIPT_SUFFIXES=[`.js`,`.mjs`,`.cjs`];function normalizeDistDir(distDir){return distDir.replaceAll(`\\`,`/`).replace(LEADING_DOT_SLASH_RE,``)}function resolveDistDir(projectDir,distDir){return isAbsolute(distDir)?distDir:resolve(projectDir,distDir)}async function walkDistTrees(absDistDir,suffixes,subtrees){return(await Promise.all(subtrees.map(async subtree=>{let root=subtree===`.`?absDistDir:join(absDistDir,subtree),entries=await readdir(root,{recursive:!0}).catch(()=>[]),out=[];for(let entry of entries)typeof entry==`string`&&suffixes.some(suffix=>entry.endsWith(suffix))&&out.push(join(root,entry));return out}))).flat()}function toPublicPath(rel,rewrites){let normalized=rel.replaceAll(`\\`,`/`);for(let{from,to}of rewrites){let prefix=from.endsWith(`/`)?from:`${from}/`;if(normalized.startsWith(prefix))return`${to}${normalized.slice(prefix.length)}`}return normalized}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 discoverByPairing(opts){let absDistDir=resolveDistDir(opts.projectDir,normalizeDistDir(opts.distDir)),jsPaths=await walkDistTrees(absDistDir,SCRIPT_SUFFIXES,opts.subtrees),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(absDistDir,jsAbs),opts.publicPathRewrites),mapPublic=toPublicPath(relative(absDistDir,mapAbs),opts.publicPathRewrites);pairs.set(mapPublic,{existingDebugId:readDebugIdFromJs(content),jsAbs,jsContent:content,jsPublic,mapAbs})}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();pair.existingDebugId||await writeFile(pair.jsAbs,injectDebugIdIntoJs(pair.jsContent,debugId),`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(file=>file!==null),sourceFileCount:pairs.size}}async function cleanupSourceMaps(files){let results=await Promise.allSettled(files.map(file=>unlink(file.absolute))),failures=[];for(let[index,result]of results.entries()){if(result.status!==`rejected`)continue;let error=result.reason;if(error.code===`ENOENT`)continue;let file=files[index];failures.push({absolute:file?.absolute??`<unknown>`,code:error.code,message:error.message??String(error)})}return{failures}}async function uploadSourceMaps({apiUrl,bundler,discovered,httpClient,releaseSlug}){let filesByPath=new Map(discovered.files.map(file=>[file.path,file])),signRequest={files:discovered.files.map(file=>({path:file.path,sizeBytes:byteLengthOf(file.content),...extractRichness(file.content)}))},signResponse=await postJson(httpClient,new URL(`/v1/releases/${encodeURIComponent(releaseSlug)}/source-maps/sign`,apiUrl),signRequest),totalBytes=0;await mapWithConcurrency(signResponse.uploads,12,async upload=>{let file=filesByPath.get(upload.path);if(!file)throw Error(`Sign response referenced unknown path "${upload.path}"`);totalBytes+=byteLengthOf(file.content),await putToR2(upload.presignedUrl,file.content)});let completeRequest={files:discovered.files.map(file=>({path:file.path,hash:file.hash,debugId:file.debugId,chunkUrl:file.chunkUrl})),sourceFileCount:discovered.sourceFileCount,bundler};return{fileCount:(await postJson(httpClient,new URL(`/v1/releases/${encodeURIComponent(releaseSlug)}/source-maps/complete`,apiUrl),completeRequest)).fileCount,totalBytes}}function byteLengthOf(content){return new TextEncoder().encode(content).byteLength}const EMPTY_RICHNESS={hasSourcesContent:!1,hasNames:!1,hasFile:!1,mappingsPresent:!1};function leafRichness(map){let sourcesContent=map.sourcesContent,names=map.names,file=map.file,mappings=map.mappings;return{hasSourcesContent:Array.isArray(sourcesContent)&&sourcesContent.length>0,hasNames:Array.isArray(names)&&names.length>0,hasFile:typeof file==`string`&&file.length>0,mappingsPresent:typeof mappings==`string`&&mappings.length>0}}function extractRichness(content){let parsed;try{parsed=JSON.parse(content)}catch{return EMPTY_RICHNESS}if(!(parsed&&typeof parsed==`object`))return EMPTY_RICHNESS;let top=parsed,sections=top.sections;if(!Array.isArray(sections))return leafRichness(top);let merged={hasSourcesContent:!1,hasNames:!1,hasFile:typeof top.file==`string`&&top.file.length>0,mappingsPresent:!1};for(let section of sections){let inner=section?.map;if(!(inner&&typeof inner==`object`))continue;let richness=leafRichness(inner);merged.hasSourcesContent||=richness.hasSourcesContent,merged.hasNames||=richness.hasNames,merged.mappingsPresent||=richness.mappingsPresent}return merged}async function postJson(httpClient,url,body){let response=await httpClient.request(new Request(url,{method:`POST`,headers:{"content-type":`application/json`,accept:`application/json`},body:JSON.stringify(body)}));if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`Source-map API request failed: POST ${url.pathname} -> ${response.status} ${detail}`)}return await response.json()}async function putToR2(presignedUrl,content){let response=await fetch(presignedUrl,{method:`PUT`,headers:{"content-type":`application/json`},body:content});if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`Release metadata file upload failed: ${response.status} ${detail}`)}}async function mapWithConcurrency(items,concurrency,fn){let cursor=0;async function worker(){for(;;){let index=cursor++;if(index>=items.length)return;let item=items[index];if(item===void 0)return;await fn(item,index)}}await Promise.all(Array.from({length:Math.min(concurrency,items.length)},()=>worker()))}export{cleanupSourceMaps,discoverByPairing,uploadSourceMaps};
1
+ import{isAbsolute,join,relative,resolve}from"node:path";import{createHash,randomUUID}from"node:crypto";import{readFile,readdir,unlink,writeFile}from"node:fs/promises";import{injectDebugIdIntoJs,injectDebugIdIntoJs as injectDebugIdIntoJs$1,readDebugIdFromJs}from"@interfere/types/data/source-maps";const LEADING_DOT_SLASH_RE=/^(\.\/)+/,SOURCEMAPPING_RE=/\/\/[#@]\s*sourceMappingURL=(\S+)\s*$/,SCRIPT_SUFFIXES=[`.js`,`.mjs`,`.cjs`];function normalizeDistDir(distDir){return distDir.replaceAll(`\\`,`/`).replace(LEADING_DOT_SLASH_RE,``)}function resolveDistDir(projectDir,distDir){return isAbsolute(distDir)?distDir:resolve(projectDir,distDir)}async function walkDistTrees(absDistDir,suffixes,subtrees){return(await Promise.all(subtrees.map(async subtree=>{let root=subtree===`.`?absDistDir:join(absDistDir,subtree),entries=await readdir(root,{recursive:!0}).catch(()=>[]),out=[];for(let entry of entries)typeof entry==`string`&&suffixes.some(suffix=>entry.endsWith(suffix))&&out.push(join(root,entry));return out}))).flat()}function toPublicPath(rel,rewrites){let normalized=rel.replaceAll(`\\`,`/`);for(let{from,to}of rewrites){let prefix=from.endsWith(`/`)?from:`${from}/`;if(normalized.startsWith(prefix))return`${to}${normalized.slice(prefix.length)}`}return normalized}function injectDebugIdIntoMap(content,debugId){let json=JSON.parse(content);return json.debugId=debugId,JSON.stringify(json)}async function discoverByPairing(opts){let absDistDir=resolveDistDir(opts.projectDir,normalizeDistDir(opts.distDir)),jsPaths=await walkDistTrees(absDistDir,SCRIPT_SUFFIXES,opts.subtrees),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(absDistDir,jsAbs),opts.publicPathRewrites),mapPublic=toPublicPath(relative(absDistDir,mapAbs),opts.publicPathRewrites);pairs.set(mapPublic,{existingDebugId:readDebugIdFromJs(content),jsAbs,jsContent:content,jsPublic,mapAbs})}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();pair.existingDebugId||await writeFile(pair.jsAbs,injectDebugIdIntoJs$1(pair.jsContent,debugId),`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(file=>file!==null),sourceFileCount:pairs.size}}async function cleanupSourceMaps(files){let results=await Promise.allSettled(files.map(file=>unlink(file.absolute))),failures=[];for(let[index,result]of results.entries()){if(result.status!==`rejected`)continue;let error=result.reason;if(error.code===`ENOENT`)continue;let file=files[index];failures.push({absolute:file?.absolute??`<unknown>`,code:error.code,message:error.message??String(error)})}return{failures}}async function uploadSourceMaps({apiUrl,bundler,discovered,httpClient,releaseSlug}){let filesByPath=new Map(discovered.files.map(file=>[file.path,file])),signRequest={files:discovered.files.map(file=>({path:file.path,sizeBytes:byteLengthOf(file.content),...extractRichness(file.content)}))},signResponse=await postJson(httpClient,new URL(`/v1/releases/${encodeURIComponent(releaseSlug)}/source-maps/sign`,apiUrl),signRequest),totalBytes=0;await mapWithConcurrency(signResponse.uploads,12,async upload=>{let file=filesByPath.get(upload.path);if(!file)throw Error(`Sign response referenced unknown path "${upload.path}"`);totalBytes+=byteLengthOf(file.content),await putToR2(upload.presignedUrl,file.content)});let completeRequest={files:discovered.files.map(file=>({path:file.path,hash:file.hash,debugId:file.debugId,chunkUrl:file.chunkUrl})),sourceFileCount:discovered.sourceFileCount,bundler};return{fileCount:(await postJson(httpClient,new URL(`/v1/releases/${encodeURIComponent(releaseSlug)}/source-maps/complete`,apiUrl),completeRequest)).fileCount,totalBytes}}function byteLengthOf(content){return new TextEncoder().encode(content).byteLength}const EMPTY_RICHNESS={hasSourcesContent:!1,hasNames:!1,hasFile:!1,mappingsPresent:!1};function leafRichness(map){let sourcesContent=map.sourcesContent,names=map.names,file=map.file,mappings=map.mappings;return{hasSourcesContent:Array.isArray(sourcesContent)&&sourcesContent.length>0,hasNames:Array.isArray(names)&&names.length>0,hasFile:typeof file==`string`&&file.length>0,mappingsPresent:typeof mappings==`string`&&mappings.length>0}}function extractRichness(content){let parsed;try{parsed=JSON.parse(content)}catch{return EMPTY_RICHNESS}if(!(parsed&&typeof parsed==`object`))return EMPTY_RICHNESS;let top=parsed,sections=top.sections;if(!Array.isArray(sections))return leafRichness(top);let merged={hasSourcesContent:!1,hasNames:!1,hasFile:typeof top.file==`string`&&top.file.length>0,mappingsPresent:!1};for(let section of sections){let inner=section?.map;if(!(inner&&typeof inner==`object`))continue;let richness=leafRichness(inner);merged.hasSourcesContent||=richness.hasSourcesContent,merged.hasNames||=richness.hasNames,merged.mappingsPresent||=richness.mappingsPresent}return merged}async function postJson(httpClient,url,body){let response=await httpClient.request(new Request(url,{method:`POST`,headers:{"content-type":`application/json`,accept:`application/json`},body:JSON.stringify(body)}));if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`Source-map API request failed: POST ${url.pathname} -> ${response.status} ${detail}`)}return await response.json()}async function putToR2(presignedUrl,content){let response=await fetch(presignedUrl,{method:`PUT`,headers:{"content-type":`application/json`},body:content});if(!response.ok){let detail=await response.text().catch(()=>``);throw Error(`Release metadata file upload failed: ${response.status} ${detail}`)}}async function mapWithConcurrency(items,concurrency,fn){let cursor=0;async function worker(){for(;;){let index=cursor++;if(index>=items.length)return;let item=items[index];if(item===void 0)return;await fn(item,index)}}await Promise.all(Array.from({length:Math.min(concurrency,items.length)},()=>worker()))}export{cleanupSourceMaps,discoverByPairing,injectDebugIdIntoJs,uploadSourceMaps};
@@ -1 +1 @@
1
- import{readFirstEnvValue}from"@interfere/types/sdk/env";import{deriveReleaseSlug}from"@interfere/types/releases/slug";import{execSync}from"node:child_process";import{releaseSourceIdEnvKeys}from"@interfere/types/integrations";function runGitCommand(command){try{let output=execSync(command,{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return output.length>0?output:null}catch{return null}}function resolveCommitSha(){return readFirstEnvValue(process.env,releaseSourceIdEnvKeys)??runGitCommand(`git rev-parse HEAD`)}function resolveReleaseSlug(){let commitSha=resolveCommitSha();return{commitSha,slug:commitSha?deriveReleaseSlug(commitSha):null}}export{resolveCommitSha,resolveReleaseSlug};
1
+ import{deriveReleaseSlug}from"@interfere/types/releases/slug";import{execSync}from"node:child_process";import{releaseSourceIdEnvKeys}from"@interfere/types/integrations";import{readFirstEnvValue}from"@interfere/types/sdk/env";function runGitCommand(command){try{let output=execSync(command,{encoding:`utf8`,stdio:[`ignore`,`pipe`,`ignore`]}).trim();return output.length>0?output:null}catch{return null}}function resolveCommitSha(){return readFirstEnvValue(process.env,releaseSourceIdEnvKeys)??runGitCommand(`git rev-parse HEAD`)}function resolveReleaseSlug(){let commitSha=resolveCommitSha();return{commitSha,slug:commitSha?deriveReleaseSlug(commitSha):null}}export{resolveCommitSha,resolveReleaseSlug};
@@ -7,12 +7,13 @@ interface InterfereServerEnv {
7
7
  }
8
8
  /**
9
9
  * Server-side gate for the proxy handler, the `captureError` /
10
- * `onRequestError` helpers, and the OTel register path. Distinct
11
- * from the browser-side `isEnabledByEnvironment` (in
12
- * `@interfere/react/internal/kernel`) because the server has
13
- * different env conventions: a configured public key is the
14
- * opt-in for proxy/server runtimes in dev. Collector ingress remains the
15
- * authority on whether local / unknown / production events are allowed.
10
+ * `onRequestError` helpers, and the OTel register path. Reads the same
11
+ * publishable `VITE_INTERFERE_PUBLIC_KEY` as the browser the key is
12
+ * not a secret, so there is no reason to demand a second server-only
13
+ * variable. Vite only inlines `VITE_*` into the client bundle, so the
14
+ * plugin mirrors it into `process.env` for the dev server. Collector
15
+ * ingress remains the authority on whether local / unknown / production
16
+ * events are allowed.
16
17
  */
17
18
  declare function isEnabledOnServer(): boolean;
18
19
  declare function readInterfereServerEnv(): InterfereServerEnv;
@@ -1 +1 @@
1
- import{API_URL}from"@interfere/constants/api";import{parseEnvValue}from"@interfere/types/sdk/env";import{resolveEnvironment}from"@interfere/types/sdk/runtime";function isEnabledOnServer(){return process.env.NODE_ENV===`production`?!0:!!process.env.INTERFERE_PUBLIC_KEY}function readInterfereServerEnv(){let nodeEnvironment=resolveEnvironment({env:process.env});return{apiUrl:parseEnvValue(process.env.INTERFERE_API_URL)??API_URL,forceEnable:!!process.env.INTERFERE_FORCE_ENABLE,nodeEnvironment,publicKey:parseEnvValue(process.env.INTERFERE_PUBLIC_KEY)}}export{isEnabledOnServer,readInterfereServerEnv};
1
+ import{API_URL}from"@interfere/constants/api";import{parseEnvValue}from"@interfere/types/sdk/env";import{resolveEnvironment}from"@interfere/types/sdk/runtime";function isEnabledOnServer(){return process.env.NODE_ENV===`production`?!0:!!process.env.VITE_INTERFERE_PUBLIC_KEY}function readInterfereServerEnv(){let nodeEnvironment=resolveEnvironment({env:process.env});return{apiUrl:parseEnvValue(process.env.INTERFERE_API_URL)??API_URL,forceEnable:!!process.env.INTERFERE_FORCE_ENABLE,nodeEnvironment,publicKey:parseEnvValue(process.env.VITE_INTERFERE_PUBLIC_KEY)}}export{isEnabledOnServer,readInterfereServerEnv};
@@ -17,7 +17,7 @@ import { ServerInstrumentationOptions } from "./instrumentation-options.mjs";
17
17
  * Idempotent — repeat calls (e.g. HMR) short-circuit on the
18
18
  * module-scoped `registered` flag.
19
19
  *
20
- * Bails silently when `INTERFERE_PUBLIC_KEY` is unset so dev runs
20
+ * Bails silently when `VITE_INTERFERE_PUBLIC_KEY` is unset so dev runs
21
21
  * without the SDK configured don't crash.
22
22
  *
23
23
  * Wired against the Node OTel SDK (`NodeTracerProvider`,
@@ -1 +1 @@
1
- import{readInterfereServerEnv}from"./env.mjs";import{PRODUCER_VERSION}from"../../version.mjs";import{resolveReleaseSlug}from"../release-slug.mjs";import{withPublicKey}from"./url.mjs";import{fetchAndCacheRemoteConfig}from"./remote-config.mjs";import{bridgeConsoleToOtel}from"./console-bridge.mjs";import{metrics,propagation}from"@opentelemetry/api";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";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{resourceFromAttributes}from"@opentelemetry/resources";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";let registered=!1;function matchesAny(url,patterns){for(let pattern of patterns)if(pattern instanceof RegExp?pattern.test(url):url.includes(pattern))return!0;return!1}async function register(opts={}){if(registered)return;let env=readInterfereServerEnv();if(!env.publicKey)return;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},resource=resourceFromAttributes({"service.name":opts.serviceName??`interfere-sdk-vite-server`,"service.namespace":`interfere`,"deployment.environment.name":env.nodeEnvironment,"telemetry.sdk.language":`nodejs`,"interfere.sdk.name":`@interfere/vite`,"interfere.sdk.version":PRODUCER_VERSION,...releaseSlug?{"release.slug":releaseSlug}:{}}),tracerProvider=new NodeTracerProvider({resource,spanProcessors:[new BaggageSpanProcessor(()=>!0),new BatchSpanProcessor(new OTLPTraceExporter({url:sinkUrl,headers:exporterHeaders}))]});propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,new W3CBaggagePropagator]})),tracerProvider.register({contextManager:new AsyncLocalStorageContextManager().enable()});let loggerProvider=new LoggerProvider({resource,processors:[new BatchLogRecordProcessor(new OTLPLogExporter({url:sinkUrl,headers:exporterHeaders}))]}),{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider);let meterProvider=new MeterProvider({resource,readers:[new PeriodicExportingMetricReader({exporter:new OTLPMetricExporter({url:sinkUrl,headers:exporterHeaders,temporalityPreference:AggregationTemporalityPreference.DELTA}),exportIntervalMillis:3e4})]});metrics.setGlobalMeterProvider(meterProvider),registerInstrumentations({tracerProvider,meterProvider,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)}})]}),opts.consoleBridge!==!1&&bridgeConsoleToOtel(),registered=!0,await fetchAndCacheRemoteConfig()}export{register};
1
+ import{PRODUCER_VERSION}from"../../version.mjs";import{resolveReleaseSlug}from"../release-slug.mjs";import{readInterfereServerEnv}from"./env.mjs";import{withPublicKey}from"./url.mjs";import{fetchAndCacheRemoteConfig}from"./remote-config.mjs";import{bridgeConsoleToOtel}from"./console-bridge.mjs";import{metrics,propagation}from"@opentelemetry/api";import{BaggageSpanProcessor}from"@opentelemetry/baggage-span-processor";import{AsyncLocalStorageContextManager}from"@opentelemetry/context-async-hooks";import{CompositePropagator,W3CBaggagePropagator,W3CTraceContextPropagator}from"@opentelemetry/core";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{resourceFromAttributes}from"@opentelemetry/resources";import{BatchLogRecordProcessor,LoggerProvider}from"@opentelemetry/sdk-logs";import{MeterProvider,PeriodicExportingMetricReader}from"@opentelemetry/sdk-metrics";import{BatchSpanProcessor}from"@opentelemetry/sdk-trace-base";import{NodeTracerProvider}from"@opentelemetry/sdk-trace-node";let registered=!1;function matchesAny(url,patterns){for(let pattern of patterns)if(pattern instanceof RegExp?pattern.test(url):url.includes(pattern))return!0;return!1}async function register(opts={}){if(registered)return;let env=readInterfereServerEnv();if(!env.publicKey)return;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},resource=resourceFromAttributes({"service.name":opts.serviceName??`interfere-sdk-vite-server`,"service.namespace":`interfere`,"deployment.environment.name":env.nodeEnvironment,"telemetry.sdk.language":`nodejs`,"interfere.sdk.name":`@interfere/vite`,"interfere.sdk.version":PRODUCER_VERSION,...releaseSlug?{"release.slug":releaseSlug}:{}}),tracerProvider=new NodeTracerProvider({resource,spanProcessors:[new BaggageSpanProcessor(()=>!0),new BatchSpanProcessor(new OTLPTraceExporter({url:sinkUrl,headers:exporterHeaders}))]});propagation.fields().length===0&&propagation.setGlobalPropagator(new CompositePropagator({propagators:[new W3CTraceContextPropagator,new W3CBaggagePropagator]})),tracerProvider.register({contextManager:new AsyncLocalStorageContextManager().enable()});let loggerProvider=new LoggerProvider({resource,processors:[new BatchLogRecordProcessor(new OTLPLogExporter({url:sinkUrl,headers:exporterHeaders}))]}),{logs:logsApi}=await import(`@opentelemetry/api-logs`);logsApi.setGlobalLoggerProvider(loggerProvider);let meterProvider=new MeterProvider({resource,readers:[new PeriodicExportingMetricReader({exporter:new OTLPMetricExporter({url:sinkUrl,headers:exporterHeaders,temporalityPreference:AggregationTemporalityPreference.DELTA}),exportIntervalMillis:3e4})]});metrics.setGlobalMeterProvider(meterProvider),registerInstrumentations({tracerProvider,meterProvider,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)}})]}),opts.consoleBridge!==!1&&bridgeConsoleToOtel(),registered=!0,await fetchAndCacheRemoteConfig()}export{register};
package/dist/package.mjs CHANGED
@@ -1 +1 @@
1
- var name=`@interfere/vite`,version=`11.0.0-canary.5`;export{name,version};
1
+ var name=`@interfere/vite`,version=`11.0.0-canary.7`;export{name,version};
package/dist/plugin.d.mts CHANGED
@@ -1,8 +1,15 @@
1
1
  import { Plugin } from "vite";
2
2
 
3
3
  //#region src/plugin.d.ts
4
- type InterfereViteMode = "auto" | "direct" | "proxy";
5
4
  interface InterfereViteOptions {
5
+ /**
6
+ * The endpoint the browser SDK posts telemetry to. Defaults to
7
+ * Interfere's endpoint. Set it to a subdomain of your own site that
8
+ * resolves to Interfere via a DNS record (e.g.
9
+ * `"https://in.example.com"`) to survive ad-blockers, or to a
10
+ * regional/local endpoint. Must be a full URL.
11
+ */
12
+ apiHost?: string;
6
13
  /**
7
14
  * Override the commit SHA used to derive the release slug. Defaults
8
15
  * to `VITE_INTERFERE_COMMIT_SHA` env then `git rev-parse HEAD`.
@@ -11,23 +18,12 @@ interface InterfereViteOptions {
11
18
  */
12
19
  commitSha?: string;
13
20
  /**
14
- * How the runtime SDK should reach the collector.
15
- *
16
- * - `"auto"` (default) detect from the surrounding plugin set:
17
- * if `tanstack-start` / `nitro` is present, choose
18
- * `"proxy"`; otherwise `"direct"`.
19
- * - `"direct"` — stamp `__INTERFERE_PUBLIC_KEY__` into the bundle
20
- * so the kernel posts ingest at the configured collector URL
21
- * directly (`in.interfere.com/v2/sink`). The only option for
22
- * vanilla Vite SPAs.
23
- * - `"proxy"` — stamp the public key and force the kernel to post
24
- * ingest at `/api/interfere/*`. The server-side handler forwards
25
- * requests through as a first-party collector proxy. Strongly
26
- * preferred for any deployment with a server origin because adblock filter
27
- * lists target the collector hostname more aggressively than
28
- * first-party paths.
21
+ * Directory (relative to the project root) where the SSR/server build
22
+ * writes its `.js.map` files, when that isn't `<build.outDir>/server`.
23
+ * Set this for server hosts that emit outside Vite's `outDir` — e.g.
24
+ * Nitro's `".output/server"`.
29
25
  */
30
- mode?: InterfereViteMode;
26
+ serverSourcemapsDir?: string;
31
27
  /**
32
28
  * `false` skips the post-build release metadata pipeline. Defaults
33
29
  * to enabled when an `INTERFERE_API_KEY` is set; defaults to
@@ -39,4 +35,4 @@ interface InterfereViteOptions {
39
35
  }
40
36
  declare function interfere(options?: InterfereViteOptions): Plugin;
41
37
  //#endregion
42
- export { InterfereViteMode, InterfereViteOptions, interfere };
38
+ export { InterfereViteOptions, interfere };
package/dist/plugin.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import{PRODUCER_VERSION}from"./version.mjs";import{discoverByPairing}from"./internal/build/source-maps.mjs";import{createSdkClient,runReleasePipeline}from"./internal/build/pipeline.mjs";import{resolveCommitSha}from"./internal/release-slug.mjs";import{API_URL}from"@interfere/constants/api";import{deriveReleaseSlug}from"@interfere/types/releases/slug";import{classifyReleasePipelineError,diagnoseRelease,isInterfereApiKey}from"@interfere/types/sdk/build-diagnostics";import{resolveDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{existsSync,readdirSync,statSync}from"node:fs";import{join,resolve}from"node:path";import{loadEnv}from"vite";const SERVER_RUNTIME_PLUGIN_HINTS=[`tanstack-start`,`nitro`,`@tanstack/react-start`],DIAGNOSTIC_CONTEXT={publicKeyEnvVar:`VITE_INTERFERE_PUBLIC_KEY`};function reportReleaseDiagnostic(logger,code,input){let diagnostic=diagnoseRelease(code,DIAGNOSTIC_CONTEXT,input);logger.warn(`[interfere] ${diagnostic.title}`);for(let line of diagnostic.details)logger.warn(`[interfere] ${line}`)}function isTruthyEnvFlag(value){if(!value)return!1;let normalized=value.trim().toLowerCase();return normalized===`1`||normalized===`true`}function isTestEnv(){return!!(process.env.VITEST||process.env.VITEST_WORKER_ID)}function pickEnvValue(env,key){let value=env[key];return value&&value.length>0?value:void 0}function failMissingPublicKey(){let lines=[`interfere() requires VITE_INTERFERE_PUBLIC_KEY to identify your project at runtime.`,`Set the VITE_INTERFERE_PUBLIC_KEY environment variable, or remove interfere() from your Vite config.`,`See: https://support.interfere.com/docs/guides/vite`];if(isTestEnv())throw Error(`Interfere: Public key not set\n${lines.join(`
1
+ import{PRODUCER_VERSION}from"./version.mjs";import{discoverByPairing}from"./internal/build/source-maps.mjs";import{createSdkClient,runReleasePipeline}from"./internal/build/pipeline.mjs";import{resolveCommitSha}from"./internal/release-slug.mjs";import{API_URL}from"@interfere/constants/api";import{deriveReleaseSlug}from"@interfere/types/releases/slug";import{classifyReleasePipelineError,diagnoseRelease,isInterfereApiKey}from"@interfere/types/sdk/build-diagnostics";import{resolveDeployEnvironment}from"@interfere/types/sdk/deploy-environment";import{existsSync,readdirSync,statSync}from"node:fs";import{join,resolve}from"node:path";import{loadEnv}from"vite";const SERVER_RUNTIME_PLUGIN_HINTS=[`tanstack-start`,`nitro`,`@tanstack/react-start`],DIAGNOSTIC_CONTEXT={publicKeyEnvVar:`VITE_INTERFERE_PUBLIC_KEY`};function reportReleaseDiagnostic(logger,code,input){let diagnostic=diagnoseRelease(code,DIAGNOSTIC_CONTEXT,input);logger.warn(`[interfere] ${diagnostic.title}`);for(let line of diagnostic.details)logger.warn(`[interfere] ${line}`)}function isTestEnv(){return!!(process.env.VITEST||process.env.VITEST_WORKER_ID)}function pickEnvValue(env,key){let value=env[key];return value&&value.length>0?value:void 0}function failMissingPublicKey(){let lines=[`interfere() requires VITE_INTERFERE_PUBLIC_KEY to identify your project at runtime.`,`Set the VITE_INTERFERE_PUBLIC_KEY environment variable, or remove interfere() from your Vite config.`,`See: https://interfere.com/docs/guides/vite`];if(isTestEnv())throw Error(`Interfere: Public key not set\n${lines.join(`
2
2
  `)}`);console.error(`
3
- ⨯ Interfere → Public key not set`);for(let i=0;i<lines.length;i+=1){let line=lines[i];if(!line)continue;let connector=i===lines.length-1?`└`:`├`;console.error(` ${connector} ${line}`)}process.exit(1)}function detectServerRuntime(config){return config.plugins.some(p=>SERVER_RUNTIME_PLUGIN_HINTS.some(hint=>p.name?.includes(hint)))}function resolveMode(optsMode,config,logger,announce){let requested=optsMode??`auto`;if(requested!==`auto`)return announce&&logger.info(`[interfere] mode=${requested} (explicit)`),requested;let detected=detectServerRuntime(config)?`proxy`:`direct`;return announce&&logger.info(`[interfere] mode=${detected} (auto-detected). Override with \`interfere({ mode: "..." })\`.`),detected}function hasFreshServerSourceMaps(root,sinceMs){if(!existsSync(root))return!1;let entries=readdirSync(root,{withFileTypes:!0});for(let entry of entries){let absolute=join(root,entry.name);if(entry.isDirectory()){if(hasFreshServerSourceMaps(absolute,sinceMs))return!0;continue}if(entry.name.endsWith(`.map`)&&statSync(absolute).mtimeMs>=sinceMs)return!0}return!1}function isUploadReady(state){return!!(state.isProductionBuild&&state.enableUpload&&state.apiKey&&state.releaseSlug&&state.commitSha&&!state.uploadStarted)}function shouldWaitForServerOutput(state){return state.mode===`proxy`&&!hasFreshServerSourceMaps(join(state.uploadOutDir,`server`),state.buildStartedAt)}function logSkippedRelease(logger,failure){if(failure.kind===`missing_source_provider`){logger.warn(`[interfere] release skipped: this surface has no version control provider configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`);return}logger.warn(`[interfere] release skipped: this surface has no deployment target configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`)}function logCleanupFailures(logger,failures){for(let failure of failures)logger.warn(`[interfere] cleanup left ${failure.absolute} on disk: ${failure.code??failure.message}`)}async function uploadSourceMaps(state){let{apiKey,apiUrl,releaseSlug,commitSha,logger,projectDir,uploadOutDir}=state;if(!isInterfereApiKey(apiKey)){reportReleaseDiagnostic(logger,`API_KEY_WRONG_PREFIX`,{apiKey});return}logger.info(`[interfere] uploading source maps for release ${releaseSlug}…`);try{let{sdk,security}=createSdkClient({apiKey,apiUrl}),[discovered,releasesConfig]=await Promise.all([discoverByPairing({projectDir,distDir:uploadOutDir,subtrees:[`.`],publicPathRewrites:[{from:`public`,to:``}]}),sdk.releases.getConfig(security)]);if(discovered.files.length===0){logger.info(`[interfere] no source maps found, skipping upload.`);return}let result=await runReleasePipeline({apiKey,apiUrl,bundler:`rollup`,buildId:commitSha,discovery:discovered,environment:state.environment,producerVersion:PRODUCER_VERSION,releasesConfig,releaseSlug});if(!result.ok){logSkippedRelease(logger,result.failure);return}logCleanupFailures(logger,result.data.cleanupFailures),logger.info(`[interfere] release ${result.data.releaseSlug} confirmed — ${result.data.fileCount} maps, ${result.data.timing.total}ms.`)}catch(err){let message=err instanceof Error?err.message:String(err);reportReleaseDiagnostic(logger,classifyReleasePipelineError(err),{errorMessage:message})}}async function uploadSourceMapsWhenReady(state){if(isUploadReady(state)){if(shouldWaitForServerOutput(state)){state.logger.info(`[interfere] waiting for Nitro server output before uploading source maps.`);return}state.uploadStarted=!0,await uploadSourceMaps(state)}}function emitSetupWarnings(state){let{logger,mode,publicKeyPresent,apiKey,releaseSlug,sourceMapsExplicit,isProductionBuild}=state;mode===`proxy`&&publicKeyPresent&&logger.info(`[interfere] mode=proxy: using VITE_INTERFERE_PUBLIC_KEY with the first-party proxy route.`),sourceMapsExplicit!==!1&&apiKey&&!releaseSlug&&logger.warn(`[interfere] No commit SHA available. Release metadata publishing is disabled. Set INTERFERE_SOURCE_ID, expose VERCEL_GIT_COMMIT_SHA / GITHUB_SHA, or run inside a git checkout.`),isProductionBuild&&sourceMapsExplicit!==!1&&!apiKey&&logger.warn("[interfere] INTERFERE_API_KEY is not set. Release metadata will not be published. Create an Interfere API key in the dashboard and set it in your build environment, or pass `interfere({ sourceMaps: false })` to suppress this warning if release metadata is intentionally handled out-of-band.")}function resolveReleaseEnv(config){return{...loadEnv(config.mode,config.envDir,``),...process.env}}function resolveReleaseIdentity(options,config){let commitSha=options.commitSha??config.env.VITE_INTERFERE_COMMIT_SHA??resolveCommitSha();return{commitSha,releaseSlug:commitSha?deriveReleaseSlug(commitSha):null}}function shouldEnableUpload({apiKey,isProductionBuild,releaseSlug,sourceMaps}){return isProductionBuild?sourceMaps??!!(apiKey&&releaseSlug):!1}function buildPluginState(options,config,announce){let logger=config.logger,mode=resolveMode(options.mode,config,logger,announce),publicKey=config.env.VITE_INTERFERE_PUBLIC_KEY,isProductionBuild=config.command===`build`;isProductionBuild&&!publicKey&&failMissingPublicKey();let releaseEnv=resolveReleaseEnv(config),pickEnv=key=>pickEnvValue(releaseEnv,key),apiKey=pickEnv(`INTERFERE_API_KEY`),apiUrl=pickEnv(`INTERFERE_API_URL`)??API_URL,{commitSha,releaseSlug}=resolveReleaseIdentity(options,config),environment=isProductionBuild?resolveDeployEnvironment(releaseEnv):null,rawBrowserApiUrl=config.env.VITE_INTERFERE_API_URL,browserApiUrl=rawBrowserApiUrl&&rawBrowserApiUrl.length>0?rawBrowserApiUrl:void 0;return announce&&emitSetupWarnings({apiKey,isProductionBuild,logger,mode,publicKeyPresent:!!publicKey,releaseSlug,sourceMapsExplicit:options.sourceMaps}),{apiKey,apiUrl,browserApiUrl:mode===`direct`?browserApiUrl:void 0,commitSha,enableUpload:shouldEnableUpload({apiKey,isProductionBuild,releaseSlug,sourceMaps:options.sourceMaps}),environment,forceEnable:isTruthyEnvFlag(config.env.VITE_INTERFERE_FORCE_ENABLE),isProductionBuild,logger,mode,projectDir:config.root,publicKey,releaseSlug,uploadOutDir:resolve(config.root,config.build.outDir),uploadStarted:!1,buildStartedAt:Date.now()}}function interfere(options={}){let state=null,forcedSourcemapOriginal,announced=!1;return{name:`interfere`,enforce:`pre`,config(userConfig,{command,mode}){if(command!==`build`||options.sourceMaps===!1)return;let root=userConfig.root?resolve(process.cwd(),userConfig.root):process.cwd();if(!pickEnvValue(loadEnv(mode,userConfig.envDir?resolve(root,userConfig.envDir):root,``),`INTERFERE_API_KEY`))return;let current=userConfig.build?.sourcemap;current!==void 0&&current!==!0&&(forcedSourcemapOriginal=current),userConfig.build={...userConfig.build,sourcemap:!0}},configResolved(config){let next=buildPluginState(options,config,!announced);announced=!0,state?.mode===`proxy`&&next.mode===`proxy`&&(next.uploadOutDir=state.uploadOutDir,next.uploadStarted=state.uploadStarted,next.buildStartedAt=state.buildStartedAt),state=next,forcedSourcemapOriginal!==void 0&&(next.logger.warn(`[interfere] Forced build.sourcemap to true (was ${JSON.stringify(forcedSourcemapOriginal)}). Source maps are required to symbolicate errors; they are uploaded then deleted from the build output, so they are never served. Pass interfere({ sourceMaps: false }) to opt out.`),forcedSourcemapOriginal=void 0)},transformIndexHtml(){let assignments=[];return state?.releaseSlug&&assignments.push(`globalThis["__INTERFERE_RELEASE_SLUG__"]=${JSON.stringify(state.releaseSlug)};`),state?.publicKey&&assignments.push(`globalThis["__INTERFERE_PUBLIC_KEY__"]=${JSON.stringify(state.publicKey)};`),state?.environment&&assignments.push(`globalThis["__INTERFERE_ENVIRONMENT__"]=${JSON.stringify(state.environment)};`),state?.forceEnable&&assignments.push(`globalThis["__INTERFERE_FORCE_ENABLE__"]=true;`),state?.mode===`proxy`&&state.publicKey&&assignments.push(`globalThis["__INTERFERE_PROXY_MODE__"]=true;`),state?.mode===`direct`&&state.browserApiUrl&&assignments.push(`globalThis["__INTERFERE_API_URL__"]=${JSON.stringify(state.browserApiUrl)};`),assignments.length===0?[]:[{tag:`script`,children:assignments.join(``),injectTo:`head-prepend`}]},async closeBundle(){state&&await uploadSourceMapsWhenReady(state)}}}export{interfere};
3
+ ⨯ Interfere → Public key not set`);for(let i=0;i<lines.length;i+=1){let line=lines[i];if(!line)continue;let connector=i===lines.length-1?`└`:`├`;console.error(` ${connector} ${line}`)}process.exit(1)}function hasServerRuntimePlugin(pluginNames){return pluginNames.some(name=>SERVER_RUNTIME_PLUGIN_HINTS.some(hint=>name.includes(hint)))}function normalizeOrigin(host){try{return new URL(host).origin}catch{return null}}function resolveApiHost(options,env){let host=options.apiHost??env.VITE_INTERFERE_API_URL;if(!host)return;let origin=normalizeOrigin(host);if(!origin)throw Error(`interfere({ apiHost }) requires a full URL, e.g. apiHost: "https://in.yourapp.com"`);return origin}function resolveBrowserConfig(options,env,command){let commitSha=options.commitSha??env.VITE_INTERFERE_COMMIT_SHA??resolveCommitSha();return{apiHost:resolveApiHost(options,env),commitSha,environment:command===`build`?resolveDeployEnvironment({...env,...process.env}):null,publicKey:env.VITE_INTERFERE_PUBLIC_KEY,releaseSlug:commitSha?deriveReleaseSlug(commitSha):null}}function browserDefine(browser){return{"import.meta.env.VITE_INTERFERE_API_HOST":JSON.stringify(browser.apiHost??null),"import.meta.env.VITE_INTERFERE_ENVIRONMENT":JSON.stringify(browser.environment),"import.meta.env.VITE_INTERFERE_RELEASE_SLUG":JSON.stringify(browser.releaseSlug)}}function hasFreshServerSourceMaps(root,sinceMs){if(!existsSync(root))return!1;let entries=readdirSync(root,{withFileTypes:!0});for(let entry of entries){let absolute=join(root,entry.name);if(entry.isDirectory()){if(hasFreshServerSourceMaps(absolute,sinceMs))return!0;continue}if(entry.name.endsWith(`.map`)&&statSync(absolute).mtimeMs>=sinceMs)return!0}return!1}function isUploadReady(state){return!!(state.isProductionBuild&&state.enableUpload&&state.apiKey&&state.releaseSlug&&state.commitSha&&!state.uploadStarted)}function shouldWaitForServerOutput(state){return state.serverBuild&&!hasFreshServerSourceMaps(state.serverSourcemapsDir,state.buildStartedAt)}function logSkippedRelease(logger,failure){if(failure.kind===`missing_source_provider`){logger.warn(`[interfere] release skipped: this surface has no version control provider configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`);return}logger.warn(`[interfere] release skipped: this surface has no deployment target configured. Configure one at https://interfere.com/~/${failure.config.org.slug}/surfaces/${failure.config.surface.slug}.`)}function logCleanupFailures(logger,failures){for(let failure of failures)logger.warn(`[interfere] cleanup left ${failure.absolute} on disk: ${failure.code??failure.message}`)}async function uploadSourceMaps(state){let{apiKey,apiUrl,releaseSlug,commitSha,logger,projectDir,uploadOutDir}=state;if(!isInterfereApiKey(apiKey)){reportReleaseDiagnostic(logger,`API_KEY_WRONG_PREFIX`,{apiKey});return}logger.info(`[interfere] uploading source maps for release ${releaseSlug}…`);try{let{sdk,security}=createSdkClient({apiKey,apiUrl}),[discovered,releasesConfig]=await Promise.all([discoverByPairing({projectDir,distDir:uploadOutDir,subtrees:[`.`],publicPathRewrites:[{from:`public`,to:``}]}),sdk.releases.getConfig(security)]);if(discovered.files.length===0){logger.info(`[interfere] no source maps found, skipping upload.`);return}let result=await runReleasePipeline({apiKey,apiUrl,bundler:`rollup`,buildId:commitSha,discovery:discovered,environment:state.environment,producerVersion:PRODUCER_VERSION,releasesConfig,releaseSlug});if(!result.ok){logSkippedRelease(logger,result.failure);return}logCleanupFailures(logger,result.data.cleanupFailures),logger.info(`[interfere] release ${result.data.releaseSlug} confirmed — ${result.data.fileCount} maps, ${result.data.timing.total}ms.`)}catch(err){let message=err instanceof Error?err.message:String(err);reportReleaseDiagnostic(logger,classifyReleasePipelineError(err),{errorMessage:message})}}async function uploadSourceMapsWhenReady(state){if(isUploadReady(state)){if(shouldWaitForServerOutput(state)){state.logger.info(`[interfere] waiting for Nitro server output before uploading source maps.`);return}state.uploadStarted=!0,await uploadSourceMaps(state)}}function emitSetupWarnings(state){let{logger,apiKey,releaseSlug,sourceMapsExplicit,isProductionBuild}=state;sourceMapsExplicit!==!1&&apiKey&&!releaseSlug&&logger.warn(`[interfere] No commit SHA available. Release metadata publishing is disabled. Set INTERFERE_SOURCE_ID, expose VERCEL_GIT_COMMIT_SHA / GITHUB_SHA, or run inside a git checkout.`),isProductionBuild&&sourceMapsExplicit!==!1&&!apiKey&&logger.warn("[interfere] INTERFERE_API_KEY is not set. Release metadata will not be published. Create an Interfere API key in the dashboard and set it in your build environment, or pass `interfere({ sourceMaps: false })` to suppress this warning if release metadata is intentionally handled out-of-band.")}function resolveReleaseEnv(config){return{...loadEnv(config.mode,config.envDir,``),...process.env}}function shouldEnableUpload({apiKey,isProductionBuild,releaseSlug,sourceMaps}){return isProductionBuild?sourceMaps??!!(apiKey&&releaseSlug):!1}function buildPluginState(options,config,browser,announce){let logger=config.logger,isProductionBuild=config.command===`build`;isProductionBuild&&!browser.publicKey&&failMissingPublicKey();let releaseEnv=resolveReleaseEnv(config),apiKey=pickEnvValue(releaseEnv,`INTERFERE_API_KEY`),apiUrl=pickEnvValue(releaseEnv,`INTERFERE_API_URL`)??API_URL,uploadOutDir=resolve(config.root,config.build.outDir),serverSourcemapsDir=options.serverSourcemapsDir?resolve(config.root,options.serverSourcemapsDir):join(uploadOutDir,`server`),serverBuild=hasServerRuntimePlugin(config.plugins.map(p=>p.name))||!!config.build?.ssr;return announce&&(logger.info(`[interfere] ingest → ${browser.apiHost??`Interfere (default)`}`),!browser.apiHost&&serverBuild&&logger.warn(`[interfere] Telemetry posts directly to Interfere. On a server-rendered app, set interfere({ apiHost }) to a subdomain of your own site to survive ad-blockers.`),emitSetupWarnings({apiKey,isProductionBuild,logger,releaseSlug:browser.releaseSlug,sourceMapsExplicit:options.sourceMaps})),{apiKey,apiUrl,commitSha:browser.commitSha,enableUpload:shouldEnableUpload({apiKey,isProductionBuild,releaseSlug:browser.releaseSlug,sourceMaps:options.sourceMaps}),environment:browser.environment,isProductionBuild,logger,serverBuild,projectDir:config.root,publicKey:browser.publicKey,releaseSlug:browser.releaseSlug,serverSourcemapsDir,uploadOutDir,uploadStarted:!1,buildStartedAt:Math.floor(Date.now()/1e3)*1e3}}function mirrorPublicKeyForDevServer(command,env){if(command!==`serve`)return;let publicKey=env.VITE_INTERFERE_PUBLIC_KEY;publicKey&&!process.env.VITE_INTERFERE_PUBLIC_KEY&&(process.env.VITE_INTERFERE_PUBLIC_KEY=publicKey)}function resolveEnvDir(userConfig){let root=userConfig.root?resolve(process.cwd(),userConfig.root):process.cwd();return userConfig.envDir?resolve(root,userConfig.envDir):root}function forceSourcemapForUpload(userConfig,options,command,env){if(!(command===`build`&&options.sourceMaps!==!1&&pickEnvValue(env,`INTERFERE_API_KEY`)))return;let previous=userConfig.build?.sourcemap;return userConfig.build={...userConfig.build,sourcemap:!0},previous===void 0||previous===!0?void 0:previous}function carryOverUploadProgress(previous,next){previous?.serverBuild&&next.serverBuild&&(next.uploadOutDir=previous.uploadOutDir,next.uploadStarted=previous.uploadStarted,next.buildStartedAt=previous.buildStartedAt)}function warnForcedSourcemap(logger,overridden){overridden!==void 0&&logger.warn(`[interfere] Forced build.sourcemap to true (was ${JSON.stringify(overridden)}). Source maps are required to symbolicate errors; they are uploaded then deleted from the build output, so they are never served. Pass interfere({ sourceMaps: false }) to opt out.`)}function interfere(options={}){let state=null,overriddenSourcemap,announced=!1;return{name:`interfere`,enforce:`pre`,config(userConfig,{command,mode}){let env=loadEnv(mode,resolveEnvDir(userConfig),``);return mirrorPublicKeyForDevServer(command,env),overriddenSourcemap=forceSourcemapForUpload(userConfig,options,command,env),{define:browserDefine(resolveBrowserConfig(options,env,command))}},configResolved(config){let next=buildPluginState(options,config,resolveBrowserConfig(options,config.env,config.command),!announced);announced=!0,carryOverUploadProgress(state,next),warnForcedSourcemap(next.logger,overriddenSourcemap),overriddenSourcemap=void 0,state=next},async closeBundle(){state&&await uploadSourceMapsWhenReady(state)}}}export{interfere};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@interfere/vite",
3
- "version": "11.0.0-canary.5",
3
+ "version": "11.0.0-canary.7",
4
4
  "license": "MIT",
5
5
  "description": "Vite build plugin and client + server initialization for Interfere.",
6
6
  "keywords": [
@@ -51,11 +51,6 @@
51
51
  "monorepo": "./src/tanstack-start.ts",
52
52
  "types": "./dist/tanstack-start.d.mts",
53
53
  "default": "./dist/tanstack-start.mjs"
54
- },
55
- "./handler": {
56
- "monorepo": "./src/handler.ts",
57
- "types": "./dist/handler.d.mts",
58
- "default": "./dist/handler.mjs"
59
54
  }
60
55
  },
61
56
  "publishConfig": {
@@ -67,11 +62,10 @@
67
62
  "typecheck": "tsgo --noEmit"
68
63
  },
69
64
  "dependencies": {
70
- "@interfere/constants": "^9.0.2-canary.0",
71
- "@interfere/helpers": "^9.0.0",
72
- "@interfere/react": "^11.0.0-canary.4",
73
- "@interfere/sdk": "^11.0.0-canary.4",
74
- "@interfere/types": "^9.0.3-canary.2",
65
+ "@interfere/constants": "^9.0.2-canary.3",
66
+ "@interfere/react": "^11.0.0-canary.7",
67
+ "@interfere/sdk": "^11.0.0-canary.7",
68
+ "@interfere/types": "^10.0.0-canary.6",
75
69
  "@opentelemetry/api": "^1.9.1",
76
70
  "@opentelemetry/api-logs": "^0.219.0",
77
71
  "@opentelemetry/baggage-span-processor": "^0.5.0",
@@ -92,7 +86,7 @@
92
86
  "@tanstack/react-start": "1.168.26",
93
87
  "react": "^19.2.6",
94
88
  "react-dom": "^19.2.6",
95
- "vite": "^8.1.0"
89
+ "vite": "^8.1.2"
96
90
  },
97
91
  "peerDependenciesMeta": {
98
92
  "@tanstack/react-start": {
@@ -109,18 +103,18 @@
109
103
  }
110
104
  },
111
105
  "devDependencies": {
112
- "@interfere/test-utils": "^9.0.0",
113
- "@interfere/typescript-config": "^9.0.0",
106
+ "@interfere/test-utils": "^9.0.1-canary.1",
107
+ "@interfere/typescript-config": "^9.0.1-canary.1",
114
108
  "@tanstack/react-start": "1.168.26",
115
- "@types/node": "^26.0.0",
109
+ "@types/node": "^26.1.0",
116
110
  "@types/react": "19.2.17",
117
111
  "@types/react-dom": "19.2.3",
118
- "@typescript/native-preview": "7.0.0-dev.20260623.1",
112
+ "@typescript/native-preview": "7.0.0-dev.20260701.1",
119
113
  "@vitest/coverage-v8": "4.1.9",
120
114
  "react": "^19.2.6",
121
115
  "react-dom": "^19.2.6",
122
116
  "tsdown": "^0.22.2",
123
- "vite": "^8.1.0",
117
+ "vite": "^8.1.2",
124
118
  "vitest": "4.1.9"
125
119
  }
126
120
  }
@@ -1,2 +0,0 @@
1
- import { handle } from "./internal/route/handle.mjs";
2
- export { handle };
package/dist/handler.mjs DELETED
@@ -1 +0,0 @@
1
- import{handle}from"./internal/route/handle.mjs";export{handle};
@@ -1,20 +0,0 @@
1
- //#region src/internal/route/handle.d.ts
2
- /**
3
- * Web-Standards `Request → Response` handler that customers wire
4
- * into their server framework's catch-all API route. Same proxy
5
- * behavior as `@interfere/next/route-handler` but built for
6
- * TanStack Start / Nitro's Web-Standards request handlers.
7
- *
8
- * Routes by HTTP method:
9
- * - `GET /sw` — serves the SDK service worker so customers get
10
- * offline-durable telemetry without shipping a SW from their
11
- * own domain.
12
- * - `GET /*` — proxies to the collector (config fetch).
13
- * - `POST /*` — proxies to the collector (OTLP ingest, replay,
14
- * session sync).
15
- * - `OPTIONS /*` — CORS preflight.
16
- * - everything else — 405.
17
- */
18
- declare function handle(request: Request): Promise<Response>;
19
- //#endregion
20
- export { handle };
@@ -1 +0,0 @@
1
- import{isEnabledOnServer}from"../server/env.mjs";import{extractSubPath,formatProxyError,forwardToCollector,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`},CORS_HEADERS={"Access-Control-Allow-Origin":`*`,"Access-Control-Allow-Methods":`GET, POST, OPTIONS`,"Access-Control-Allow-Headers":`Content-Type`,"Access-Control-Max-Age":`86400`},DISABLED_RESPONSE=()=>new Response(null,{status:204});async function handle(request){let method=request.method;return method===`OPTIONS`?new Response(null,{status:200,headers:CORS_HEADERS}):method===`GET`?await handleGet(request):method===`POST`?await handlePost(request):new Response(null,{status:405})}async function handleGet(request){let subPath=extractSubPath(request);if(subPath===`/sw`)return new Response(SW_SCRIPT,{status:200,headers:SW_HEADERS});if(!isEnabledOnServer())return DISABLED_RESPONSE();let env=resolveAuthenticatedEnv();try{return await forwardToCollector(request,env,subPath)}catch(error){let detail=formatProxyError(error);return console.error(`[interfere] Proxy ${request.method} ${subPath} failed: ${detail.lines.join(` | `)}`),Response.json({code:`INTERFERE_PROXY_ERROR`,message:detail.message},{status:502})}}async function handlePost(request){if(!isEnabledOnServer())return DISABLED_RESPONSE();let env=resolveAuthenticatedEnv(),subPath=extractSubPath(request);try{return await forwardToCollector(request,env,subPath)}catch(error){let detail=formatProxyError(error);return console.error(`[interfere] Proxy ${request.method} ${subPath} failed: ${detail.lines.join(` | `)}`),Response.json({code:`INTERFERE_PROXY_ERROR`,message:detail.message},{status:502})}}export{handle};
@@ -1,33 +0,0 @@
1
- import { InterfereServerEnv } from "../server/env.mjs";
2
-
3
- //#region src/internal/route/proxy.d.ts
4
- /**
5
- * Clone the inbound request's headers, removing entries that should
6
- * never cross the proxy boundary. Returns a mutable `Headers` so
7
- * callers can `.set()`/`.delete()` upstream-specific values without
8
- * having to merge record literals (which lose case-insensitivity).
9
- */
10
- declare function forwardedHeaders(request: Request): Headers;
11
- interface AuthenticatedEnv {
12
- apiUrl: string;
13
- forceEnable: InterfereServerEnv["forceEnable"];
14
- }
15
- declare function resolveAuthenticatedEnv(): AuthenticatedEnv;
16
- declare function extractSubPath(request: Request): string;
17
- /**
18
- * Preserve any query string verbatim when forwarding upstream.
19
- *
20
- * The browser SDK uses `?_pv=…` and `?pk=…` (and `?_pt=…` in direct
21
- * mode) as a `navigator.sendBeacon` fallback when `visibilitychange→
22
- * hidden` aborts the keepalive fetch path, so authentication works without
23
- * custom headers. The collector's `otlpProducerGuard` and `surfacePkAuth`
24
- * accept those values verbatim — but only if the proxy actually forwards them.
25
- */
26
- declare function extractSearch(request: Request): string;
27
- declare function formatProxyError(error: unknown): {
28
- message: string;
29
- lines: string[];
30
- };
31
- declare function forwardToCollector(request: Request, env: AuthenticatedEnv, subPath: string): Promise<Response>;
32
- //#endregion
33
- export { AuthenticatedEnv, extractSearch, extractSubPath, formatProxyError, forwardToCollector, forwardedHeaders, resolveAuthenticatedEnv };
@@ -1 +0,0 @@
1
- import{readInterfereServerEnv}from"../server/env.mjs";import{resolveRoutePrefix}from"@interfere/constants/route-prefix";import{omitUndefined}from"@interfere/helpers/omit-undefined";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=readInterfereServerEnv();return{apiUrl:env.apiUrl,forceEnable:env.forceEnable}}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 extractSearch(request){return new URL(request.url).search}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}}async function forwardToCollector(request,env,subPath){let url=`${env.apiUrl}${subPath}${extractSearch(request)}`,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`),env.forceEnable&&headers.set(`x-interfere-force-enable`,`1`);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?(console.error(`[interfere] Upstream ${upstream.status} for ${request.method} ${subPath}: ${text}`),Response.json({code:`INTERFERE_UPSTREAM_ERROR`,message:text},{status:upstream.status})):upstream.status>=400?(console.warn(`[interfere] Upstream ${upstream.status} for ${request.method} ${subPath}: ${text}`),Response.json({code:`INTERFERE_UPSTREAM_WARNING`,message:text},{status:upstream.status})):(console.error(`[interfere] 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{extractSearch,extractSubPath,formatProxyError,forwardToCollector,forwardedHeaders,resolveAuthenticatedEnv};