@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 +13 -41
- package/dist/init.d.mts +4 -3
- package/dist/init.mjs +1 -1
- package/dist/internal/browser-globals.d.mts +4 -0
- package/dist/internal/browser-globals.mjs +1 -0
- package/dist/internal/build/pipeline.d.mts +1 -1
- package/dist/internal/build/pipeline.mjs +1 -1
- package/dist/internal/build/source-maps.d.mts +2 -2
- package/dist/internal/build/source-maps.mjs +1 -1
- package/dist/internal/release-slug.mjs +1 -1
- package/dist/internal/server/env.d.mts +7 -6
- package/dist/internal/server/env.mjs +1 -1
- package/dist/internal/server/register.d.mts +1 -1
- package/dist/internal/server/register.mjs +1 -1
- package/dist/package.mjs +1 -1
- package/dist/plugin.d.mts +14 -18
- package/dist/plugin.mjs +2 -2
- package/package.json +11 -17
- package/dist/handler.d.mts +0 -2
- package/dist/handler.mjs +0 -1
- package/dist/internal/route/handle.d.mts +0 -20
- package/dist/internal/route/handle.mjs +0 -1
- package/dist/internal/route/proxy.d.mts +0 -33
- package/dist/internal/route/proxy.mjs +0 -1
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://
|
|
5
|
-
<img src="https://
|
|
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://
|
|
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
|
-
|
|
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
|
|
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
|
|
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: () =>
|
|
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
|
-
},
|
|
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
|
|
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 @@
|
|
|
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{
|
|
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*$/,
|
|
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{
|
|
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.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* authority on whether local / unknown / production
|
|
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.
|
|
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 `
|
|
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{
|
|
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.
|
|
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
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
71
|
-
"@interfere/
|
|
72
|
-
"@interfere/
|
|
73
|
-
"@interfere/
|
|
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.
|
|
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.
|
|
113
|
-
"@interfere/typescript-config": "^9.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.
|
|
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.
|
|
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.
|
|
117
|
+
"vite": "^8.1.2",
|
|
124
118
|
"vitest": "4.1.9"
|
|
125
119
|
}
|
|
126
120
|
}
|
package/dist/handler.d.mts
DELETED
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};
|