@interfere/vite 0.1.1 → 10.0.1-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -30
- package/dist/handler.d.mts +2 -0
- package/dist/handler.mjs +1 -0
- package/dist/init.d.mts +10 -3
- package/dist/init.d.mts.map +1 -1
- package/dist/init.mjs +1 -11
- package/dist/init.mjs.map +1 -1
- package/dist/internal/build/pipeline.d.mts +64 -0
- package/dist/internal/build/pipeline.d.mts.map +1 -0
- package/dist/internal/build/pipeline.mjs +1 -0
- package/dist/internal/build/pipeline.mjs.map +1 -0
- package/dist/internal/build/source-maps.d.mts +56 -0
- package/dist/internal/build/source-maps.d.mts.map +1 -0
- package/dist/internal/build/source-maps.mjs +1 -0
- package/dist/internal/build/source-maps.mjs.map +1 -0
- package/dist/internal/release-slug.d.mts +10 -0
- package/dist/internal/release-slug.d.mts.map +1 -0
- package/dist/internal/release-slug.mjs +1 -0
- package/dist/internal/release-slug.mjs.map +1 -0
- package/dist/internal/route/handle.d.mts +20 -0
- package/dist/internal/route/handle.d.mts.map +1 -0
- package/dist/internal/route/handle.mjs +1 -0
- package/dist/internal/route/handle.mjs.map +1 -0
- package/dist/internal/route/proxy.d.mts +33 -0
- package/dist/internal/route/proxy.d.mts.map +1 -0
- package/dist/internal/route/proxy.mjs +1 -0
- package/dist/internal/route/proxy.mjs.map +1 -0
- package/dist/internal/server/capture.d.mts +6 -0
- package/dist/internal/server/capture.d.mts.map +1 -0
- package/dist/internal/server/capture.mjs +1 -0
- package/dist/internal/server/capture.mjs.map +1 -0
- package/dist/internal/server/console-bridge.d.mts +23 -0
- package/dist/internal/server/console-bridge.d.mts.map +1 -0
- package/dist/internal/server/console-bridge.mjs +1 -0
- package/dist/internal/server/console-bridge.mjs.map +1 -0
- package/dist/internal/server/env.d.mts +22 -0
- package/dist/internal/server/env.d.mts.map +1 -0
- package/dist/internal/server/env.mjs +1 -0
- package/dist/internal/server/env.mjs.map +1 -0
- package/dist/internal/server/instrumentation-options.d.mts +44 -0
- package/dist/internal/server/instrumentation-options.d.mts.map +1 -0
- package/dist/internal/server/instrumentation-options.mjs +1 -0
- package/dist/internal/server/register.d.mts +30 -0
- package/dist/internal/server/register.d.mts.map +1 -0
- package/dist/internal/server/register.mjs +1 -0
- package/dist/internal/server/register.mjs.map +1 -0
- package/dist/internal/server/remote-config.d.mts +5 -0
- package/dist/internal/server/remote-config.d.mts.map +1 -0
- package/dist/internal/server/remote-config.mjs +1 -0
- package/dist/internal/server/remote-config.mjs.map +1 -0
- package/dist/internal/server/trace-meta.d.mts +34 -0
- package/dist/internal/server/trace-meta.d.mts.map +1 -0
- package/dist/internal/server/trace-meta.mjs +1 -0
- package/dist/internal/server/trace-meta.mjs.map +1 -0
- package/dist/internal/server/traceparent.d.mts +17 -0
- package/dist/internal/server/traceparent.d.mts.map +1 -0
- package/dist/internal/server/traceparent.mjs +1 -0
- package/dist/internal/server/traceparent.mjs.map +1 -0
- package/dist/internal/server/types.d.mts +17 -0
- package/dist/internal/server/types.d.mts.map +1 -0
- package/dist/internal/server/types.mjs +1 -0
- package/dist/internal/server/url.d.mts +4 -0
- package/dist/internal/server/url.d.mts.map +1 -0
- package/dist/internal/server/url.mjs +1 -0
- package/dist/internal/server/url.mjs.map +1 -0
- package/dist/package.mjs +1 -5
- package/dist/plugin.d.mts +35 -3
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +3 -34
- package/dist/plugin.mjs.map +1 -1
- package/dist/provider.d.mts +20 -0
- package/dist/provider.d.mts.map +1 -0
- package/dist/provider.mjs +1 -0
- package/dist/provider.mjs.map +1 -0
- package/dist/server.d.mts +6 -0
- package/dist/server.mjs +1 -0
- package/dist/version.mjs +1 -5
- package/dist/version.mjs.map +1 -1
- package/dist/vite-env.d.mts +1 -0
- package/package.json +70 -21
|
@@ -0,0 +1,33 @@
|
|
|
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-ingestion
|
|
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 };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.d.mts","names":[],"sources":["../../../src/internal/route/proxy.ts"],"mappings":";;;;;AAyCA;;;;iBAAgB,gBAAA,CAAiB,OAAA,EAAS,OAAA,GAAU,OAAO;AAAA,UAW1C,gBAAA;EACf,MAAA;EACA,WAAA,EAAa,kBAAkB;AAAA;AAAA,iBAGjB,uBAAA,CAAA,GAA2B,gBAAgB;AAAA,iBAQ3C,cAAA,CAAe,OAAgB,EAAP,OAAO;;;;;;;AAXd;AAGjC;;iBA+BgB,aAAA,CAAc,OAAgB,EAAP,OAAO;AAAA,iBAI9B,gBAAA,CAAiB,KAAA;EAC/B,OAAA;EACA,KAAA;AAAA;AAAA,iBAwBoB,kBAAA,CACpB,OAAA,EAAS,OAAA,EACT,GAAA,EAAK,gBAAA,EACL,OAAA,WACC,OAAA,CAAQ,QAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
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`?await request.text():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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy.mjs","names":[],"sources":["../../../src/internal/route/proxy.ts"],"sourcesContent":["import { resolveRoutePrefix } from \"@interfere/constants/route-prefix\";\nimport { omitUndefined } from \"@interfere/helpers/omit-undefined\";\nimport { extractCountryHeader } from \"@interfere/types/sdk/geo\";\n\nimport {\n type InterfereServerEnv,\n readInterfereServerEnv,\n} from \"../server/env.js\";\n\n// Never forward customer API keys from browser-originated ingestion requests.\nconst STRIPPED_AUTH_HEADERS = [\n \"authorization\",\n \"x-api-key\",\n \"x-interfere-api-key\",\n] as const;\n\n// Headers tied to the inbound HTTP transaction. These must not leak\n// onto the outbound `fetch` because the runtime computes them fresh\n// for the new body / connection, and forwarding them stale either\n// silently corrupts the request or hangs it until a timeout fires.\n// Same set Next's proxy strips (see `@interfere/next/internal/route/\n// proxy.ts` for the full incident postmortem on `content-length`).\nconst STRIPPED_TRANSPORT_HEADERS = [\n \"host\",\n \"content-length\",\n \"connection\",\n \"keep-alive\",\n \"transfer-encoding\",\n \"te\",\n \"trailer\",\n \"upgrade\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n] as const;\n\n/**\n * Clone the inbound request's headers, removing entries that should\n * never cross the proxy boundary. Returns a mutable `Headers` so\n * callers can `.set()`/`.delete()` upstream-specific values without\n * having to merge record literals (which lose case-insensitivity).\n */\nexport function forwardedHeaders(request: Request): Headers {\n const headers = new Headers(request.headers);\n for (const name of STRIPPED_AUTH_HEADERS) {\n headers.delete(name);\n }\n for (const name of STRIPPED_TRANSPORT_HEADERS) {\n headers.delete(name);\n }\n return headers;\n}\n\nexport interface AuthenticatedEnv {\n apiUrl: string;\n forceEnable: InterfereServerEnv[\"forceEnable\"];\n}\n\nexport function resolveAuthenticatedEnv(): AuthenticatedEnv {\n const env = readInterfereServerEnv();\n return {\n apiUrl: env.apiUrl,\n forceEnable: env.forceEnable,\n };\n}\n\nexport function extractSubPath(request: Request): string {\n const prefix = resolveRoutePrefix();\n const url = new URL(request.url);\n const idx = url.pathname.indexOf(prefix);\n if (idx === -1) {\n return \"/\";\n }\n const remainder = url.pathname.slice(idx + prefix.length);\n if (remainder === \"\") {\n return \"/\";\n }\n return remainder.startsWith(\"/\") ? remainder : `/${remainder}`;\n}\n\n/**\n * Preserve any query string verbatim when forwarding upstream.\n *\n * The browser SDK uses `?_pv=…` and `?pk=…` (and `?_pt=…` in direct-ingestion\n * mode) as a `navigator.sendBeacon` fallback when `visibilitychange→\n * hidden` aborts the keepalive fetch path, so authentication works without\n * custom headers. The collector's `otlpProducerGuard` and `surfacePkAuth`\n * accept those values verbatim — but only if the proxy actually forwards them.\n */\nexport function extractSearch(request: Request): string {\n return new URL(request.url).search;\n}\n\nexport function formatProxyError(error: unknown): {\n message: string;\n lines: string[];\n} {\n if (!(error instanceof Error)) {\n return { message: String(error), lines: [String(error)] };\n }\n\n const lines = [`${error.name}: ${error.message}`];\n\n if (\"cause\" in error && error.cause) {\n const cause =\n error.cause instanceof Error ? error.cause.message : String(error.cause);\n lines.push(`cause: ${cause}`);\n }\n\n if (\n \"code\" in error &&\n typeof (error as NodeJS.ErrnoException).code === \"string\"\n ) {\n lines.push(`code: ${(error as NodeJS.ErrnoException).code}`);\n }\n\n return { message: `${error.name}: ${error.message}`, lines };\n}\n\nexport async function forwardToCollector(\n request: Request,\n env: AuthenticatedEnv,\n subPath: string\n): Promise<Response> {\n const url = `${env.apiUrl}${subPath}${extractSearch(request)}`;\n const hasBody = request.method !== \"GET\" && request.method !== \"HEAD\";\n const body = hasBody ? await request.text() : undefined;\n\n const headers = forwardedHeaders(request);\n // Default to JSON only when the inbound request didn't pin a\n // content-type — non-`/v1/ingest` paths relay the body bytes\n // verbatim, so honour whatever the caller said it was sending.\n if (!headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n if (env.forceEnable) {\n headers.set(\"x-interfere-force-enable\", \"1\");\n }\n const country = extractCountryHeader(request);\n if (country !== null) {\n headers.set(\"x-country-code\", country);\n }\n\n const upstream = await fetch(url, {\n ...omitUndefined({ body }),\n method: request.method,\n headers,\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!upstream.ok) {\n const text = await upstream.text().catch(() => \"\");\n\n if (upstream.status >= 500) {\n console.error(\n `[interfere] Upstream ${upstream.status} for ${request.method} ${subPath}: ${text}`\n );\n return Response.json(\n { code: \"INTERFERE_UPSTREAM_ERROR\", message: text },\n { status: upstream.status }\n );\n }\n if (upstream.status >= 400) {\n console.warn(\n `[interfere] Upstream ${upstream.status} for ${request.method} ${subPath}: ${text}`\n );\n return Response.json(\n { code: \"INTERFERE_UPSTREAM_WARNING\", message: text },\n { status: upstream.status }\n );\n }\n\n console.error(\n `[interfere] Upstream ${upstream.status} for ${request.method} ${subPath}: ${text}`\n );\n return Response.json(\n { code: \"INTERFERE_UPSTREAM_ERROR\", message: text },\n { status: upstream.status }\n );\n }\n\n return new Response(upstream.body, {\n status: upstream.status,\n headers: {\n \"content-type\":\n upstream.headers.get(\"content-type\") ?? \"application/json\",\n },\n });\n}\n"],"mappings":"gPAUA,MAAM,sBAAwB,CAC5B,gBACA,YACA,qBACF,EAQM,2BAA6B,CACjC,OACA,iBACA,aACA,aACA,oBACA,KACA,UACA,UACA,qBACA,qBACF,EAQA,SAAgB,iBAAiB,QAA2B,CAC1D,IAAM,QAAU,IAAI,QAAQ,QAAQ,OAAO,EAC3C,IAAK,IAAM,QAAQ,sBACjB,QAAQ,OAAO,IAAI,EAErB,IAAK,IAAM,QAAQ,2BACjB,QAAQ,OAAO,IAAI,EAErB,OAAO,OACT,CAOA,SAAgB,yBAA4C,CAC1D,IAAM,IAAM,uBAAuB,EACnC,MAAO,CACL,OAAQ,IAAI,OACZ,YAAa,IAAI,WACnB,CACF,CAEA,SAAgB,eAAe,QAA0B,CACvD,IAAM,OAAS,mBAAmB,EAC5B,IAAM,IAAI,IAAI,QAAQ,GAAG,EACzB,IAAM,IAAI,SAAS,QAAQ,MAAM,EACvC,GAAI,MAAQ,GACV,MAAO,IAET,IAAM,UAAY,IAAI,SAAS,MAAM,IAAM,OAAO,MAAM,EAIxD,OAHI,YAAc,GACT,IAEF,UAAU,WAAW,GAAG,EAAI,UAAY,IAAI,WACrD,CAWA,SAAgB,cAAc,QAA0B,CACtD,OAAO,IAAI,IAAI,QAAQ,GAAG,EAAE,MAC9B,CAEA,SAAgB,iBAAiB,MAG/B,CACA,GAAI,EAAE,iBAAiB,OACrB,MAAO,CAAE,QAAS,OAAO,KAAK,EAAG,MAAO,CAAC,OAAO,KAAK,CAAC,CAAE,EAG1D,IAAM,MAAQ,CAAC,GAAG,MAAM,KAAK,IAAI,MAAM,SAAS,EAEhD,GAAI,UAAW,OAAS,MAAM,MAAO,CACnC,IAAM,MACJ,MAAM,iBAAiB,MAAQ,MAAM,MAAM,QAAU,OAAO,MAAM,KAAK,EACzE,MAAM,KAAK,UAAU,OAAO,CAC9B,CASA,MANE,SAAU,OACV,OAAQ,MAAgC,MAAS,UAEjD,MAAM,KAAK,SAAU,MAAgC,MAAM,EAGtD,CAAE,QAAS,GAAG,MAAM,KAAK,IAAI,MAAM,UAAW,KAAM,CAC7D,CAEA,eAAsB,mBACpB,QACA,IACA,QACmB,CACnB,IAAM,IAAM,GAAG,IAAI,SAAS,UAAU,cAAc,OAAO,IAErD,KADU,QAAQ,SAAW,OAAS,QAAQ,SAAW,OACxC,MAAM,QAAQ,KAAK,EAAI,IAAA,GAExC,QAAU,iBAAiB,OAAO,EAInC,QAAQ,IAAI,cAAc,GAC7B,QAAQ,IAAI,eAAgB,kBAAkB,EAE5C,IAAI,aACN,QAAQ,IAAI,2BAA4B,GAAG,EAE7C,IAAM,QAAU,qBAAqB,OAAO,EACxC,UAAY,MACd,QAAQ,IAAI,iBAAkB,OAAO,EAGvC,IAAM,SAAW,MAAM,MAAM,IAAK,CAChC,GAAG,cAAc,CAAE,IAAK,CAAC,EACzB,OAAQ,QAAQ,OAChB,QACA,OAAQ,YAAY,QAAQ,GAAM,CACpC,CAAC,EAED,GAAI,CAAC,SAAS,GAAI,CAChB,IAAM,KAAO,MAAM,SAAS,KAAK,EAAE,UAAY,EAAE,EAwBjD,OAtBI,SAAS,QAAU,KACrB,QAAQ,MACN,wBAAwB,SAAS,OAAO,OAAO,QAAQ,OAAO,GAAG,QAAQ,IAAI,MAC/E,EACO,SAAS,KACd,CAAE,KAAM,2BAA4B,QAAS,IAAK,EAClD,CAAE,OAAQ,SAAS,MAAO,CAC5B,GAEE,SAAS,QAAU,KACrB,QAAQ,KACN,wBAAwB,SAAS,OAAO,OAAO,QAAQ,OAAO,GAAG,QAAQ,IAAI,MAC/E,EACO,SAAS,KACd,CAAE,KAAM,6BAA8B,QAAS,IAAK,EACpD,CAAE,OAAQ,SAAS,MAAO,CAC5B,IAGF,QAAQ,MACN,wBAAwB,SAAS,OAAO,OAAO,QAAQ,OAAO,GAAG,QAAQ,IAAI,MAC/E,EACO,SAAS,KACd,CAAE,KAAM,2BAA4B,QAAS,IAAK,EAClD,CAAE,OAAQ,SAAS,MAAO,CAC5B,EACF,CAEA,OAAO,IAAI,SAAS,SAAS,KAAM,CACjC,OAAQ,SAAS,OACjB,QAAS,CACP,eACE,SAAS,QAAQ,IAAI,cAAc,GAAK,kBAC5C,CACF,CAAC,CACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.d.mts","names":[],"sources":["../../../src/internal/server/capture.ts"],"mappings":";;;iBAiGgB,YAAA,CAAa,KAAA,WAAgB,GAAA,GAAM,mBAAmB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isEnabledOnServer}from"./env.mjs";import{isPluginEnabled}from"./remote-config.mjs";import{MECHANISM_TYPE,isNonErrorException,toException}from"@interfere/types/sdk/errors";import{SpanKind,SpanStatusCode,context,trace}from"@opentelemetry/api";const DEFAULT_CAPTURE_ERROR_MECHANISM={type:MECHANISM_TYPE.node.captureError,handled:!0},seen=new WeakSet;function buildAttrs(value,mechanism,ctx){let attrs={"interfere.exception.mechanism":mechanism.type,"interfere.exception.handled":String(mechanism.handled)};return ctx?.request?.method&&(attrs[`http.request.method`]=ctx.request.method),ctx?.request?.path&&(attrs[`url.path`]=ctx.request.path),isNonErrorException(value)?(attrs[`exception.type`]=value.type,attrs[`exception.message`]=value.value,attrs[`interfere.exception.kind`]=`non-error`,attrs):(attrs[`exception.type`]=value.name,attrs[`exception.message`]=value.message,value.stack&&(attrs[`exception.stacktrace`]=value.stack),attrs)}function recordException(error,mechanism,ctx){let value=toException(error),attrs=buildAttrs(value,mechanism,ctx),tracer=trace.getTracer(`@interfere/vite/server`),active=trace.getActiveSpan();if(active){active.addEvent(`exception`,attrs),mechanism.handled||active.setStatus({code:SpanStatusCode.ERROR,message:isNonErrorException(value)?value.value:value.message});return}let span=tracer.startSpan(`interfere.captureError`,{kind:SpanKind.INTERNAL},context.active());span.addEvent(`exception`,attrs),mechanism.handled||span.setStatus({code:SpanStatusCode.ERROR,message:isNonErrorException(value)?value.value:value.message}),span.end()}function captureError(error,ctx){if(isEnabledOnServer()&&isPluginEnabled(`errors`)){if(error instanceof Error){if(seen.has(error))return;seen.add(error)}recordException(error,ctx?.mechanism??DEFAULT_CAPTURE_ERROR_MECHANISM,ctx)}}export{captureError};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.mjs","names":[],"sources":["../../../src/internal/server/capture.ts"],"sourcesContent":["import {\n isNonErrorException,\n MECHANISM_TYPE,\n type NonErrorException,\n toException,\n} from \"@interfere/types/sdk/errors\";\nimport type { ErrorMechanism } from \"@interfere/types/sdk/plugins/payload/errors\";\n\nimport { context, SpanKind, SpanStatusCode, trace } from \"@opentelemetry/api\";\n\nimport { isEnabledOnServer } from \"./env.js\";\nimport { isPluginEnabled } from \"./remote-config.js\";\nimport type { CaptureErrorContext } from \"./types.js\";\n\nconst TRACER_NAME = \"@interfere/vite/server\";\n\nconst DEFAULT_CAPTURE_ERROR_MECHANISM: ErrorMechanism = {\n type: MECHANISM_TYPE.node.captureError,\n handled: true,\n};\n\n// Per-process dedupe so a thrown handler error captured by both the\n// caller's `try { } catch { captureError(e) }` and the framework's\n// own onRequestError hook only emits one exception event. WeakSet\n// so GC'd errors don't pin memory.\nconst seen = new WeakSet<Error>();\n\nfunction buildAttrs(\n value: Error | NonErrorException,\n mechanism: ErrorMechanism,\n ctx: CaptureErrorContext | undefined\n): Record<string, string> {\n const attrs: Record<string, string> = {\n \"interfere.exception.mechanism\": mechanism.type,\n \"interfere.exception.handled\": String(mechanism.handled),\n };\n if (ctx?.request?.method) {\n attrs[\"http.request.method\"] = ctx.request.method;\n }\n if (ctx?.request?.path) {\n attrs[\"url.path\"] = ctx.request.path;\n }\n if (isNonErrorException(value)) {\n attrs[\"exception.type\"] = value.type;\n attrs[\"exception.message\"] = value.value;\n attrs[\"interfere.exception.kind\"] = \"non-error\";\n return attrs;\n }\n attrs[\"exception.type\"] = value.name;\n attrs[\"exception.message\"] = value.message;\n if (value.stack) {\n attrs[\"exception.stacktrace\"] = value.stack;\n }\n return attrs;\n}\n\nfunction recordException(\n error: unknown,\n mechanism: ErrorMechanism,\n ctx: CaptureErrorContext | undefined\n): void {\n const value = toException(error);\n const attrs = buildAttrs(value, mechanism, ctx);\n\n // Prefer the active span (Nitro / TanStack Start request span).\n // Fall back to a fresh single-event span when no active span is\n // available — frameworks sometimes call error hooks from\n // background contexts (background revalidation, failed prerender)\n // where the request span is no longer current.\n const tracer = trace.getTracer(TRACER_NAME);\n const active = trace.getActiveSpan();\n if (active) {\n active.addEvent(\"exception\", attrs);\n if (!mechanism.handled) {\n active.setStatus({\n code: SpanStatusCode.ERROR,\n message: isNonErrorException(value) ? value.value : value.message,\n });\n }\n return;\n }\n\n const span = tracer.startSpan(\n \"interfere.captureError\",\n { kind: SpanKind.INTERNAL },\n context.active()\n );\n span.addEvent(\"exception\", attrs);\n if (!mechanism.handled) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: isNonErrorException(value) ? value.value : value.message,\n });\n }\n span.end();\n}\n\nexport function captureError(error: unknown, ctx?: CaptureErrorContext): void {\n if (!isEnabledOnServer()) {\n return;\n }\n if (!isPluginEnabled(\"errors\")) {\n return;\n }\n\n if (error instanceof Error) {\n if (seen.has(error)) {\n return;\n }\n seen.add(error);\n }\n\n recordException(\n error,\n ctx?.mechanism ?? DEFAULT_CAPTURE_ERROR_MECHANISM,\n ctx\n );\n}\n"],"mappings":"wPAcA,MAEM,gCAAkD,CACtD,KAAM,eAAe,KAAK,aAC1B,QAAS,EACX,EAMM,KAAO,IAAI,QAEjB,SAAS,WACP,MACA,UACA,IACwB,CACxB,IAAM,MAAgC,CACpC,gCAAiC,UAAU,KAC3C,8BAA+B,OAAO,UAAU,OAAO,CACzD,EAkBA,OAjBI,KAAK,SAAS,SAChB,MAAM,uBAAyB,IAAI,QAAQ,QAEzC,KAAK,SAAS,OAChB,MAAM,YAAc,IAAI,QAAQ,MAE9B,oBAAoB,KAAK,GAC3B,MAAM,kBAAoB,MAAM,KAChC,MAAM,qBAAuB,MAAM,MACnC,MAAM,4BAA8B,YAC7B,QAET,MAAM,kBAAoB,MAAM,KAChC,MAAM,qBAAuB,MAAM,QAC/B,MAAM,QACR,MAAM,wBAA0B,MAAM,OAEjC,MACT,CAEA,SAAS,gBACP,MACA,UACA,IACM,CACN,IAAM,MAAQ,YAAY,KAAK,EACzB,MAAQ,WAAW,MAAO,UAAW,GAAG,EAOxC,OAAS,MAAM,UAAU,wBAAW,EACpC,OAAS,MAAM,cAAc,EACnC,GAAI,OAAQ,CACV,OAAO,SAAS,YAAa,KAAK,EAC7B,UAAU,SACb,OAAO,UAAU,CACf,KAAM,eAAe,MACrB,QAAS,oBAAoB,KAAK,EAAI,MAAM,MAAQ,MAAM,OAC5D,CAAC,EAEH,MACF,CAEA,IAAM,KAAO,OAAO,UAClB,yBACA,CAAE,KAAM,SAAS,QAAS,EAC1B,QAAQ,OAAO,CACjB,EACA,KAAK,SAAS,YAAa,KAAK,EAC3B,UAAU,SACb,KAAK,UAAU,CACb,KAAM,eAAe,MACrB,QAAS,oBAAoB,KAAK,EAAI,MAAM,MAAQ,MAAM,OAC5D,CAAC,EAEH,KAAK,IAAI,CACX,CAEA,SAAgB,aAAa,MAAgB,IAAiC,CACvE,qBAAkB,GAGlB,gBAAgB,QAAQ,EAI7B,IAAI,iBAAiB,MAAO,CAC1B,GAAI,KAAK,IAAI,KAAK,EAChB,OAEF,KAAK,IAAI,KAAK,CAChB,CAEA,gBACE,MACA,KAAK,WAAa,gCAClB,GACF,CANA,CAOF"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/internal/server/console-bridge.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Patches `console.{debug,log,info,warn,error}` to additionally emit
|
|
4
|
+
* OTel `LogRecord`s alongside the original output. Every log record
|
|
5
|
+
* carries the active span's context (so trace ↔ log correlation
|
|
6
|
+
* works in dashboards) and any `Error` arg gets unpacked into
|
|
7
|
+
* semconv exception attrs.
|
|
8
|
+
*
|
|
9
|
+
* Returns a disposer that restores the original `console.*` methods.
|
|
10
|
+
*
|
|
11
|
+
* Cycle protection: `emitting` guards against the bridge re-entering
|
|
12
|
+
* itself if a logger backend itself logs to console (which would
|
|
13
|
+
* recursively emit forever). Failures are bounded-rate logged via
|
|
14
|
+
* the original `console.error` so a broken backend doesn't drown
|
|
15
|
+
* the customer's logs in bridge-failure messages.
|
|
16
|
+
*
|
|
17
|
+
* Sibling of `@interfere/next/internal/server/console-bridge` — same
|
|
18
|
+
* bridge, same trade-offs, intentionally kept in the Vite adapter's
|
|
19
|
+
* source tree so SDKs do not share build/runtime internals.
|
|
20
|
+
*/
|
|
21
|
+
declare function bridgeConsoleToOtel(): () => void;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { bridgeConsoleToOtel };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-bridge.d.mts","names":[],"sources":["../../../src/internal/server/console-bridge.ts"],"mappings":";;AAoFA;;;;AAAmC;;;;;;;;;;;;;;iBAAnB,mBAAA,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{context,trace}from"@opentelemetry/api";import{logs}from"@opentelemetry/api-logs";const CONSOLE_METHODS=[`debug`,`log`,`info`,`warn`,`error`],SEVERITY={debug:5,log:9,info:9,warn:13,error:17};function stringify(value){if(typeof value==`string`)return value;if(value instanceof Error)return value.stack??`${value.name}: ${value.message}`;try{return JSON.stringify(value)}catch{return String(value)}}function extractException(args){for(let arg of args)if(arg instanceof Error)return arg}function buildExceptionAttributes(args){let exception=extractException(args);if(!exception)return;let attrs={"exception.type":exception.name,"exception.message":exception.message};return exception.stack&&(attrs[`exception.stacktrace`]=exception.stack),attrs}function bridgeConsoleToOtel(){let logger=logs.getLogger(`@interfere/vite/console`),emitting=!1,bridgeFailures=0,original={debug:console.debug,log:console.log,info:console.info,warn:console.warn,error:console.error};function emitLog(orig,severity,method,body,activeContext,activeSpan,attributes){if(!emitting){emitting=!0;try{logger.emit({severityNumber:severity,severityText:method.toUpperCase(),body,...activeSpan?{context:activeContext}:{},...attributes?{attributes}:{}})}catch(error){bridgeFailures++,bridgeFailures%100==1&&orig.call(console,`[interfere] console→OTel bridge emission failed (${bridgeFailures} total):`,error)}finally{emitting=!1}}}for(let method of CONSOLE_METHODS){let orig=original[method],severity=SEVERITY[method];console[method]=(...args)=>{orig.apply(console,args);let body=args.map(stringify).join(` `),activeContext=context.active(),activeSpan=trace.getSpan(activeContext),attributes=buildExceptionAttributes(args);queueMicrotask(()=>{emitLog(orig,severity,method,body,activeContext,activeSpan,attributes)})}}return()=>{for(let method of CONSOLE_METHODS)console[method]=original[method]}}export{bridgeConsoleToOtel};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"console-bridge.mjs","names":[],"sources":["../../../src/internal/server/console-bridge.ts"],"sourcesContent":["import { context, trace } from \"@opentelemetry/api\";\nimport { logs, type SeverityNumber } from \"@opentelemetry/api-logs\";\n\nconst ATTR_EXCEPTION_TYPE = \"exception.type\" as const;\nconst ATTR_EXCEPTION_MESSAGE = \"exception.message\" as const;\nconst ATTR_EXCEPTION_STACKTRACE = \"exception.stacktrace\" as const;\n\nconst CONSOLE_METHODS = [\"debug\", \"log\", \"info\", \"warn\", \"error\"] as const;\ntype ConsoleMethod = (typeof CONSOLE_METHODS)[number];\n\nconst SEVERITY: Record<ConsoleMethod, SeverityNumber> = {\n debug: 5,\n log: 9,\n info: 9,\n warn: 13,\n error: 17,\n};\n\n/**\n * Bound at first failure log; subsequent failures only re-log every\n * Nth occurrence so a misconfigured logger backend doesn't itself\n * spam the console with bridge errors at line rate.\n */\nconst BRIDGE_FAILURE_LOG_INTERVAL = 100;\n\nfunction stringify(value: unknown): string {\n if (typeof value === \"string\") {\n return value;\n }\n if (value instanceof Error) {\n return value.stack ?? `${value.name}: ${value.message}`;\n }\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n}\n\nfunction extractException(args: unknown[]): Error | undefined {\n for (const arg of args) {\n if (arg instanceof Error) {\n return arg;\n }\n }\n return;\n}\n\nfunction buildExceptionAttributes(\n args: unknown[]\n): Record<string, string> | undefined {\n const exception = extractException(args);\n if (!exception) {\n return;\n }\n const attrs: Record<string, string> = {\n [ATTR_EXCEPTION_TYPE]: exception.name,\n [ATTR_EXCEPTION_MESSAGE]: exception.message,\n };\n if (exception.stack) {\n attrs[ATTR_EXCEPTION_STACKTRACE] = exception.stack;\n }\n return attrs;\n}\n\n/**\n * Patches `console.{debug,log,info,warn,error}` to additionally emit\n * OTel `LogRecord`s alongside the original output. Every log record\n * carries the active span's context (so trace ↔ log correlation\n * works in dashboards) and any `Error` arg gets unpacked into\n * semconv exception attrs.\n *\n * Returns a disposer that restores the original `console.*` methods.\n *\n * Cycle protection: `emitting` guards against the bridge re-entering\n * itself if a logger backend itself logs to console (which would\n * recursively emit forever). Failures are bounded-rate logged via\n * the original `console.error` so a broken backend doesn't drown\n * the customer's logs in bridge-failure messages.\n *\n * Sibling of `@interfere/next/internal/server/console-bridge` — same\n * bridge, same trade-offs, intentionally kept in the Vite adapter's\n * source tree so SDKs do not share build/runtime internals.\n */\nexport function bridgeConsoleToOtel(): () => void {\n const logger = logs.getLogger(\"@interfere/vite/console\");\n let emitting = false;\n let bridgeFailures = 0;\n\n const original: Record<ConsoleMethod, (...args: unknown[]) => void> = {\n debug: console.debug,\n log: console.log,\n info: console.info,\n warn: console.warn,\n error: console.error,\n };\n\n function emitLog(\n orig: (...args: unknown[]) => void,\n severity: SeverityNumber,\n method: ConsoleMethod,\n body: string,\n activeContext: ReturnType<typeof context.active>,\n activeSpan: ReturnType<typeof trace.getSpan>,\n attributes: Record<string, string> | undefined\n ): void {\n if (emitting) {\n return;\n }\n emitting = true;\n try {\n logger.emit({\n severityNumber: severity,\n severityText: method.toUpperCase(),\n body,\n ...(activeSpan ? { context: activeContext } : {}),\n ...(attributes ? { attributes } : {}),\n });\n } catch (error) {\n bridgeFailures++;\n if (bridgeFailures % BRIDGE_FAILURE_LOG_INTERVAL === 1) {\n orig.call(\n console,\n `[interfere] console→OTel bridge emission failed (${bridgeFailures} total):`,\n error\n );\n }\n } finally {\n emitting = false;\n }\n }\n\n for (const method of CONSOLE_METHODS) {\n const orig = original[method];\n const severity = SEVERITY[method];\n console[method] = (...args: unknown[]) => {\n orig.apply(console, args);\n\n const body = args.map(stringify).join(\" \");\n const activeContext = context.active();\n const activeSpan = trace.getSpan(activeContext);\n const attributes = buildExceptionAttributes(args);\n\n // Defer emission via microtask so the synchronous return path\n // out of `console.X` doesn't include the LoggerProvider's\n // batch logic. Customer code's `console.error(...)` call\n // should feel synchronous; the OTel emission rides the next\n // microtask. `queueMicrotask` (not `setTimeout(0)`):\n // macrotask-deferred emissions risk being lost on hard\n // process exit.\n queueMicrotask(() => {\n emitLog(\n orig,\n severity,\n method,\n body,\n activeContext,\n activeSpan,\n attributes\n );\n });\n };\n }\n\n return () => {\n for (const method of CONSOLE_METHODS) {\n console[method] = original[method];\n }\n };\n}\n"],"mappings":"wFAGA,MAIM,gBAAkB,CAAC,QAAS,MAAO,OAAQ,OAAQ,OAAO,EAG1D,SAAkD,CACtD,MAAO,EACP,IAAK,EACL,KAAM,EACN,KAAM,GACN,MAAO,EACT,EASA,SAAS,UAAU,MAAwB,CACzC,GAAI,OAAO,OAAU,SACnB,OAAO,MAET,GAAI,iBAAiB,MACnB,OAAO,MAAM,OAAS,GAAG,MAAM,KAAK,IAAI,MAAM,UAEhD,GAAI,CACF,OAAO,KAAK,UAAU,KAAK,CAC7B,MAAQ,CACN,OAAO,OAAO,KAAK,CACrB,CACF,CAEA,SAAS,iBAAiB,KAAoC,CAC5D,IAAK,IAAM,OAAO,KAChB,GAAI,eAAe,MACjB,OAAO,GAIb,CAEA,SAAS,yBACP,KACoC,CACpC,IAAM,UAAY,iBAAiB,IAAI,EACvC,GAAI,CAAC,UACH,OAEF,IAAM,MAAgC,CACnC,iBAAsB,UAAU,KAChC,oBAAyB,UAAU,OACtC,EAIA,OAHI,UAAU,QACZ,MAAM,wBAA6B,UAAU,OAExC,KACT,CAqBA,SAAgB,qBAAkC,CAChD,IAAM,OAAS,KAAK,UAAU,yBAAyB,EACnD,SAAW,GACX,eAAiB,EAEf,SAAgE,CACpE,MAAO,QAAQ,MACf,IAAK,QAAQ,IACb,KAAM,QAAQ,KACd,KAAM,QAAQ,KACd,MAAO,QAAQ,KACjB,EAEA,SAAS,QACP,KACA,SACA,OACA,KACA,cACA,WACA,WACM,CACF,aAGJ,UAAW,GACX,GAAI,CACF,OAAO,KAAK,CACV,eAAgB,SAChB,aAAc,OAAO,YAAY,EACjC,KACA,GAAI,WAAa,CAAE,QAAS,aAAc,EAAI,CAAC,EAC/C,GAAI,WAAa,CAAE,UAAW,EAAI,CAAC,CACrC,CAAC,CACH,OAAS,MAAO,CACd,iBACI,eAAiB,KAAgC,GACnD,KAAK,KACH,QACA,oDAAoD,eAAe,UACnE,KACF,CAEJ,QAAU,CACR,SAAW,EACb,CApBW,CAqBb,CAEA,IAAK,IAAM,UAAU,gBAAiB,CACpC,IAAM,KAAO,SAAS,QAChB,SAAW,SAAS,QAC1B,QAAQ,SAAW,GAAG,OAAoB,CACxC,KAAK,MAAM,QAAS,IAAI,EAExB,IAAM,KAAO,KAAK,IAAI,SAAS,EAAE,KAAK,GAAG,EACnC,cAAgB,QAAQ,OAAO,EAC/B,WAAa,MAAM,QAAQ,aAAa,EACxC,WAAa,yBAAyB,IAAI,EAShD,mBAAqB,CACnB,QACE,KACA,SACA,OACA,KACA,cACA,WACA,UACF,CACF,CAAC,CACH,CACF,CAEA,UAAa,CACX,IAAK,IAAM,UAAU,gBACnB,QAAQ,QAAU,SAAS,OAE/B,CACF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Env } from "@interfere/types/sdk/runtime";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/server/env.d.ts
|
|
4
|
+
interface InterfereServerEnv {
|
|
5
|
+
readonly apiUrl: string;
|
|
6
|
+
readonly forceEnable: boolean;
|
|
7
|
+
readonly nodeEnvironment: Env;
|
|
8
|
+
readonly publicKey: string | null;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Server-side gate for the proxy handler, the `captureError` /
|
|
12
|
+
* `onRequestError` helpers, and the OTel register path. Distinct
|
|
13
|
+
* from the browser-side `isEnabledByEnvironment` (in
|
|
14
|
+
* `@interfere/react/internal/kernel`) because the server has
|
|
15
|
+
* different env conventions: a configured public key is the
|
|
16
|
+
* opt-in for proxy/server runtimes in dev. Collector ingress remains the
|
|
17
|
+
* authority on whether local / unknown / production events are allowed.
|
|
18
|
+
*/
|
|
19
|
+
declare function isEnabledOnServer(): boolean;
|
|
20
|
+
declare function readInterfereServerEnv(): InterfereServerEnv;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { InterfereServerEnv, isEnabledOnServer, readInterfereServerEnv };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.mts","names":[],"sources":["../../../src/internal/server/env.ts"],"mappings":";;;UAKiB,kBAAA;EAAA,SACN,MAAA;EAAA,SACA,WAAA;EAAA,SACA,eAAA,EAAiB,GAAG;EAAA,SACpB,SAAA;AAAA;;;;;;;AAAS;AAYpB;;iBAAgB,iBAAA,CAAA;AAAA,iBAOA,sBAAA,CAAA,GAA0B,kBAAkB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{API_URL}from"@interfere/constants/api";import{parseEnvValue}from"@interfere/types/sdk/env";import{normalizeEnv}from"@interfere/types/sdk/runtime";function isEnabledOnServer(){return process.env.NODE_ENV===`production`?!0:!!process.env.INTERFERE_PUBLIC_KEY}function readInterfereServerEnv(){let nodeEnvironment=normalizeEnv(process.env.NODE_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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.mjs","names":[],"sources":["../../../src/internal/server/env.ts"],"sourcesContent":["import { API_URL } from \"@interfere/constants/api\";\nimport { parseEnvValue } from \"@interfere/types/sdk/env\";\nimport type { Env } from \"@interfere/types/sdk/runtime\";\nimport { normalizeEnv } from \"@interfere/types/sdk/runtime\";\n\nexport interface InterfereServerEnv {\n readonly apiUrl: string;\n readonly forceEnable: boolean;\n readonly nodeEnvironment: Env;\n readonly publicKey: string | null;\n}\n\n/**\n * Server-side gate for the proxy handler, the `captureError` /\n * `onRequestError` helpers, and the OTel register path. Distinct\n * from the browser-side `isEnabledByEnvironment` (in\n * `@interfere/react/internal/kernel`) because the server has\n * different env conventions: a configured public key is the\n * opt-in for proxy/server runtimes in dev. Collector ingress remains the\n * authority on whether local / unknown / production events are allowed.\n */\nexport function isEnabledOnServer(): boolean {\n if (process.env[\"NODE_ENV\"] === \"production\") {\n return true;\n }\n return Boolean(process.env[\"INTERFERE_PUBLIC_KEY\"]);\n}\n\nexport function readInterfereServerEnv(): InterfereServerEnv {\n const nodeEnvironment = normalizeEnv(process.env[\"NODE_ENV\"]);\n\n return {\n apiUrl: parseEnvValue(process.env[\"INTERFERE_API_URL\"]) ?? API_URL,\n forceEnable: Boolean(process.env[\"INTERFERE_FORCE_ENABLE\"]),\n nodeEnvironment,\n publicKey: parseEnvValue(process.env[\"INTERFERE_PUBLIC_KEY\"]),\n };\n}\n"],"mappings":"yJAqBA,SAAgB,mBAA6B,CAI3C,OAHI,QAAQ,IAAI,WAAgB,aACvB,GAEF,EAAQ,QAAQ,IAAI,oBAC7B,CAEA,SAAgB,wBAA6C,CAC3D,IAAM,gBAAkB,aAAa,QAAQ,IAAI,QAAW,EAE5D,MAAO,CACL,OAAQ,cAAc,QAAQ,IAAI,iBAAoB,GAAK,QAC3D,YAAa,EAAQ,QAAQ,IAAI,uBACjC,gBACA,UAAW,cAAc,QAAQ,IAAI,oBAAuB,CAC9D,CACF"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//#region src/internal/server/instrumentation-options.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Per-instrumentation tuning for the server-side `register()`.
|
|
4
|
+
* Mirrors the browser-side `TracingOptions` — same names, same
|
|
5
|
+
* semantics — so a customer running both client + server with the
|
|
6
|
+
* same SDK has one mental model.
|
|
7
|
+
*
|
|
8
|
+
* Slimmer than `@interfere/next/internal/server/instrumentation-options`
|
|
9
|
+
* by design: the Vite SDK doesn't expose the `_internal*` fan-out
|
|
10
|
+
* processors / readers (those live behind `@vercel/otel` composition
|
|
11
|
+
* in the Next adapter, which is internal-only per the parity
|
|
12
|
+
* decisions).
|
|
13
|
+
*/
|
|
14
|
+
interface ServerInstrumentationOptions {
|
|
15
|
+
/**
|
|
16
|
+
* `false` disables the `console.*` → OTel `LogRecord` bridge.
|
|
17
|
+
* Defaults to enabled. Customers running their own structured
|
|
18
|
+
* logger (Pino, Winston, etc.) that already emits OTel logs can
|
|
19
|
+
* opt out to avoid double-emit.
|
|
20
|
+
*/
|
|
21
|
+
consoleBridge?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* URL patterns the undici fetch instrumentation skips (no spans
|
|
24
|
+
* created for these). The collector OTLP endpoint is always
|
|
25
|
+
* skipped automatically — without this the SDK would trace its
|
|
26
|
+
* own export requests in an infinite loop.
|
|
27
|
+
*/
|
|
28
|
+
ignoreUrls?: (string | RegExp)[];
|
|
29
|
+
/**
|
|
30
|
+
* URL patterns the undici fetch instrumentation injects W3C
|
|
31
|
+
* `traceparent` + `baggage` headers on for cross-process trace
|
|
32
|
+
* correlation. Same-origin requests aren't a concept on the
|
|
33
|
+
* server the way they are in a browser, so the allowlist is the
|
|
34
|
+
* only propagation control.
|
|
35
|
+
*/
|
|
36
|
+
propagateContextUrls?: (string | RegExp)[];
|
|
37
|
+
/**
|
|
38
|
+
* Override the OTel `service.name` resource attribute. Defaults
|
|
39
|
+
* to `"interfere-sdk-vite-server"`.
|
|
40
|
+
*/
|
|
41
|
+
serviceName?: string;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { ServerInstrumentationOptions };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumentation-options.d.mts","names":[],"sources":["../../../src/internal/server/instrumentation-options.ts"],"mappings":";;AAYA;;;;;;;;;;;UAAiB,4BAAA;;;;;;;EAOf,aAAA;;;;;;;EAOA,UAAA,aAAuB,MAAA;;;;;;;;EAQvB,oBAAA,aAAiC,MAAM;;;;;EAKvC,WAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ServerInstrumentationOptions } from "./instrumentation-options.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/server/register.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* One-line server-side bootstrap for Nitro / TanStack Start. Customer
|
|
6
|
+
* Nitro server entry (or any server-only init module) becomes:
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { register } from "@interfere/vite/server";
|
|
10
|
+
* await register();
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Wires private `NodeTracerProvider` / `LoggerProvider` /
|
|
14
|
+
* `MeterProvider` against the global slots, installs the
|
|
15
|
+
* `console.*` → OTel bridge, and fetches the remote-config gate.
|
|
16
|
+
*
|
|
17
|
+
* Idempotent — repeat calls (e.g. HMR) short-circuit on the
|
|
18
|
+
* module-scoped `registered` flag.
|
|
19
|
+
*
|
|
20
|
+
* Bails silently when `INTERFERE_PUBLIC_KEY` is unset so dev runs
|
|
21
|
+
* without the SDK configured don't crash.
|
|
22
|
+
*
|
|
23
|
+
* Wired against the Node OTel SDK (`NodeTracerProvider`,
|
|
24
|
+
* `AsyncLocalStorageContextManager`) — only ships into the Node
|
|
25
|
+
* runtime. Cloudflare Workers / Deno presets need a separate edge
|
|
26
|
+
* stub (Phase 5).
|
|
27
|
+
*/
|
|
28
|
+
declare function register(opts?: ServerInstrumentationOptions): Promise<void>;
|
|
29
|
+
//#endregion
|
|
30
|
+
export { register };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.d.mts","names":[],"sources":["../../../src/internal/server/register.ts"],"mappings":";;;;;AA2EA;;;;;;;;AAEU;;;;;;;;;;;;;;iBAFY,QAAA,CACpB,IAAA,GAAM,4BAAA,GACL,OAAO"}
|
|
@@ -0,0 +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??`unknown`,"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};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register.mjs","names":[],"sources":["../../../src/internal/server/register.ts"],"sourcesContent":["import type { AttributeValue, Span } from \"@opentelemetry/api\";\nimport { metrics, propagation } from \"@opentelemetry/api\";\nimport { BaggageSpanProcessor } from \"@opentelemetry/baggage-span-processor\";\nimport { AsyncLocalStorageContextManager } from \"@opentelemetry/context-async-hooks\";\nimport {\n CompositePropagator,\n W3CBaggagePropagator,\n W3CTraceContextPropagator,\n} from \"@opentelemetry/core\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-http\";\nimport {\n AggregationTemporalityPreference,\n OTLPMetricExporter,\n} from \"@opentelemetry/exporter-metrics-otlp-http\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-http\";\nimport { registerInstrumentations } from \"@opentelemetry/instrumentation\";\nimport { UndiciInstrumentation } from \"@opentelemetry/instrumentation-undici\";\nimport { resourceFromAttributes } from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport { BatchSpanProcessor } from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\n\nimport { PRODUCER_VERSION } from \"../../version.js\";\nimport { resolveReleaseSlug } from \"../release-slug.js\";\nimport { bridgeConsoleToOtel } from \"./console-bridge.js\";\nimport { readInterfereServerEnv } from \"./env.js\";\nimport type { ServerInstrumentationOptions } from \"./instrumentation-options.js\";\nimport { fetchAndCacheRemoteConfig } from \"./remote-config.js\";\nimport { withPublicKey } from \"./url.js\";\n\nconst DEFAULT_SERVICE_NAME = \"interfere-sdk-vite-server\";\nconst SERVICE_NAMESPACE = \"interfere\";\n\nlet registered = false;\n\nfunction matchesAny(url: string, patterns: (string | RegExp)[]): boolean {\n for (const pattern of patterns) {\n if (pattern instanceof RegExp ? pattern.test(url) : url.includes(pattern)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * One-line server-side bootstrap for Nitro / TanStack Start. Customer\n * Nitro server entry (or any server-only init module) becomes:\n *\n * ```ts\n * import { register } from \"@interfere/vite/server\";\n * await register();\n * ```\n *\n * Wires private `NodeTracerProvider` / `LoggerProvider` /\n * `MeterProvider` against the global slots, installs the\n * `console.*` → OTel bridge, and fetches the remote-config gate.\n *\n * Idempotent — repeat calls (e.g. HMR) short-circuit on the\n * module-scoped `registered` flag.\n *\n * Bails silently when `INTERFERE_PUBLIC_KEY` is unset so dev runs\n * without the SDK configured don't crash.\n *\n * Wired against the Node OTel SDK (`NodeTracerProvider`,\n * `AsyncLocalStorageContextManager`) — only ships into the Node\n * runtime. Cloudflare Workers / Deno presets need a separate edge\n * stub (Phase 5).\n */\nexport async function register(\n opts: ServerInstrumentationOptions = {}\n): Promise<void> {\n if (registered) {\n return;\n }\n\n const env = readInterfereServerEnv();\n if (!env.publicKey) {\n return;\n }\n\n const { slug: releaseSlug } = resolveReleaseSlug();\n if (!releaseSlug) {\n console.warn(\n \"[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.\"\n );\n }\n\n const sinkUrl = withPublicKey(`${env.apiUrl}/v2/sink`, env.publicKey);\n // Always-on ignore: the OTLP exporter posting to /v2/sink would\n // otherwise be re-traced by UndiciInstrumentation in an infinite\n // loop.\n const ignoreUrls = [sinkUrl, ...(opts.ignoreUrls ?? [])];\n const propagateContextUrls = opts.propagateContextUrls ?? [];\n\n const exporterHeaders: Record<string, string> = {\n \"x-interfere-producer-version\": PRODUCER_VERSION,\n };\n\n const resourceAttributes: Record<string, AttributeValue> = {\n \"service.name\": opts.serviceName ?? DEFAULT_SERVICE_NAME,\n \"service.namespace\": SERVICE_NAMESPACE,\n \"deployment.environment.name\": env.nodeEnvironment ?? \"unknown\",\n \"telemetry.sdk.language\": \"nodejs\",\n \"interfere.sdk.name\": \"@interfere/vite\",\n \"interfere.sdk.version\": PRODUCER_VERSION,\n ...(releaseSlug ? { \"release.slug\": releaseSlug } : {}),\n };\n\n const resource = resourceFromAttributes(resourceAttributes);\n\n // `BaggageSpanProcessor` runs at span start so every span carries\n // the full baggage attribute set when batched. Default `keyFilter`\n // accepts everything; the browser SDK only writes `interfere.*`-\n // prefixed entries via baggage propagation, so the server side\n // stays scoped without an explicit filter.\n const tracerProvider = new NodeTracerProvider({\n resource,\n spanProcessors: [\n new BaggageSpanProcessor(() => true),\n new BatchSpanProcessor(\n new OTLPTraceExporter({ url: sinkUrl, headers: exporterHeaders })\n ),\n ],\n });\n\n // Composed W3C trace context + baggage. Only set the global\n // propagator when none is registered — customers who installed\n // their own propagator (B3, Jaeger, composite, …) are preserved.\n // Mirrors the browser-side guard in\n // `react/internal/otel/provider.ts`.\n if (propagation.fields().length === 0) {\n propagation.setGlobalPropagator(\n new CompositePropagator({\n propagators: [\n new W3CTraceContextPropagator(),\n new W3CBaggagePropagator(),\n ],\n })\n );\n }\n\n tracerProvider.register({\n contextManager: new AsyncLocalStorageContextManager().enable(),\n });\n\n const loggerProvider = new LoggerProvider({\n resource,\n processors: [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({ url: sinkUrl, headers: exporterHeaders })\n ),\n ],\n });\n // `logs.setGlobalLoggerProvider` is the single global slot OTel\n // exposes. Customers with their own LoggerProvider boot last and\n // win; we accept the trade-off — same constraint we live with on\n // the global propagator and context manager.\n const { logs: logsApi } = await import(\"@opentelemetry/api-logs\");\n logsApi.setGlobalLoggerProvider(loggerProvider);\n\n // `DELTA` temporality matches the browser SDK's primary metric\n // reader so server + client datapoints land at the same cadence.\n const meterProvider = new MeterProvider({\n resource,\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({\n url: sinkUrl,\n headers: exporterHeaders,\n temporalityPreference: AggregationTemporalityPreference.DELTA,\n }),\n exportIntervalMillis: 30_000,\n }),\n ],\n });\n metrics.setGlobalMeterProvider(meterProvider);\n\n registerInstrumentations({\n tracerProvider,\n meterProvider,\n instrumentations: [\n new UndiciInstrumentation({\n ignoreRequestHook: (req: { origin: string; path: string }) => {\n const url = `${req.origin}${req.path}`;\n return matchesAny(url, ignoreUrls);\n },\n requestHook: (span: Span, req: { origin: string; path: string }) => {\n const url = `${req.origin}${req.path}`;\n if (matchesAny(url, propagateContextUrls)) {\n span.setAttribute(\"interfere.propagated\", true);\n }\n },\n }),\n ],\n });\n\n if (opts.consoleBridge !== false) {\n bridgeConsoleToOtel();\n }\n\n registered = true;\n\n await fetchAndCacheRemoteConfig();\n}\n"],"mappings":"qzCAwCA,IAAI,WAAa,GAEjB,SAAS,WAAW,IAAa,SAAwC,CACvE,IAAK,IAAM,WAAW,SACpB,GAAI,mBAAmB,OAAS,QAAQ,KAAK,GAAG,EAAI,IAAI,SAAS,OAAO,EACtE,MAAO,GAGX,MAAO,EACT,CA0BA,eAAsB,SACpB,KAAqC,CAAC,EACvB,CACf,GAAI,WACF,OAGF,IAAM,IAAM,uBAAuB,EACnC,GAAI,CAAC,IAAI,UACP,OAGF,GAAM,CAAE,KAAM,aAAgB,mBAAmB,EAC5C,aACH,QAAQ,KACN,qLACF,EAGF,IAAM,QAAU,cAAc,GAAG,IAAI,OAAO,UAAW,IAAI,SAAS,EAI9D,WAAa,CAAC,QAAS,GAAI,KAAK,YAAc,CAAC,CAAE,EACjD,qBAAuB,KAAK,sBAAwB,CAAC,EAErD,gBAA0C,CAC9C,+BAAgC,gBAClC,EAYM,SAAW,uBAAuB,CATtC,eAAgB,KAAK,aAAe,4BACpC,oBAAqB,YACrB,8BAA+B,IAAI,iBAAmB,UACtD,yBAA0B,SAC1B,qBAAsB,kBACtB,wBAAyB,iBACzB,GAAI,YAAc,CAAE,eAAgB,WAAY,EAAI,CAAC,CAGE,CAAC,EAOpD,eAAiB,IAAI,mBAAmB,CAC5C,SACA,eAAgB,CACd,IAAI,yBAA2B,EAAI,EACnC,IAAI,mBACF,IAAI,kBAAkB,CAAE,IAAK,QAAS,QAAS,eAAgB,CAAC,CAClE,CACF,CACF,CAAC,EAOG,YAAY,OAAO,EAAE,SAAW,GAClC,YAAY,oBACV,IAAI,oBAAoB,CACtB,YAAa,CACX,IAAI,0BACJ,IAAI,oBACN,CACF,CAAC,CACH,EAGF,eAAe,SAAS,CACtB,eAAgB,IAAI,gCAAgC,EAAE,OAAO,CAC/D,CAAC,EAED,IAAM,eAAiB,IAAI,eAAe,CACxC,SACA,WAAY,CACV,IAAI,wBACF,IAAI,gBAAgB,CAAE,IAAK,QAAS,QAAS,eAAgB,CAAC,CAChE,CACF,CACF,CAAC,EAKK,CAAE,KAAM,SAAY,MAAM,OAAO,2BACvC,QAAQ,wBAAwB,cAAc,EAI9C,IAAM,cAAgB,IAAI,cAAc,CACtC,SACA,QAAS,CACP,IAAI,8BAA8B,CAChC,SAAU,IAAI,mBAAmB,CAC/B,IAAK,QACL,QAAS,gBACT,sBAAuB,iCAAiC,KAC1D,CAAC,EACD,qBAAsB,GACxB,CAAC,CACH,CACF,CAAC,EACD,QAAQ,uBAAuB,aAAa,EAE5C,yBAAyB,CACvB,eACA,cACA,iBAAkB,CAChB,IAAI,sBAAsB,CACxB,kBAAoB,KAEX,WAAW,GADH,IAAI,SAAS,IAAI,OACT,UAAU,EAEnC,aAAc,KAAY,MAA0C,CAE9D,WAAW,GADA,IAAI,SAAS,IAAI,OACZ,oBAAoB,GACtC,KAAK,aAAa,uBAAwB,EAAI,CAElD,CACF,CAAC,CACH,CACF,CAAC,EAEG,KAAK,gBAAkB,IACzB,oBAAoB,EAGtB,WAAa,GAEb,MAAM,0BAA0B,CAClC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-config.d.mts","names":[],"sources":["../../../src/internal/server/remote-config.ts"],"mappings":";iBAWsB,yBAAA,CAAA,GAA6B,OAAO;AAAA,iBAoC1C,eAAA,CAAgB,MAAc"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isEnabledOnServer,readInterfereServerEnv}from"./env.mjs";import{withPublicKey}from"./url.mjs";import{API_PATHS}from"@interfere/constants/api";let cachedConfig=null;async function fetchAndCacheRemoteConfig(){if(!isEnabledOnServer())return;let env=readInterfereServerEnv();if(env.publicKey!==null)try{let url=withPublicKey(`${env.apiUrl}${API_PATHS.CONFIG}`,env.publicKey),response=await fetch(url,{method:`GET`,headers:{"content-type":`application/json`},signal:AbortSignal.timeout(1e4)});if(!response.ok)return;let config=await response.json();config?.plugins&&(cachedConfig=config.plugins)}catch{}}function isPluginEnabled(plugin){return cachedConfig?cachedConfig[plugin]!==!1:!0}export{fetchAndCacheRemoteConfig,isPluginEnabled};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-config.mjs","names":[],"sources":["../../../src/internal/server/remote-config.ts"],"sourcesContent":["import { API_PATHS } from \"@interfere/constants/api\";\nimport type {\n RemoteConfig,\n RemotePluginConfig,\n} from \"@interfere/types/sdk/remote-config\";\n\nimport { isEnabledOnServer, readInterfereServerEnv } from \"./env.js\";\nimport { withPublicKey } from \"./url.js\";\n\nlet cachedConfig: RemotePluginConfig | null = null;\n\nexport async function fetchAndCacheRemoteConfig(): Promise<void> {\n if (!isEnabledOnServer()) {\n return;\n }\n\n const env = readInterfereServerEnv();\n if (env.publicKey === null) {\n return;\n }\n\n try {\n const url = withPublicKey(\n `${env.apiUrl}${API_PATHS.CONFIG}`,\n env.publicKey\n );\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n \"content-type\": \"application/json\",\n },\n signal: AbortSignal.timeout(10_000),\n });\n\n if (!response.ok) {\n return;\n }\n\n const config = (await response.json()) as RemoteConfig;\n if (config?.plugins) {\n cachedConfig = config.plugins;\n }\n } catch {\n // Fail silently — all plugins remain enabled\n }\n}\n\nexport function isPluginEnabled(plugin: string): boolean {\n if (!cachedConfig) {\n return true;\n }\n return cachedConfig[plugin as keyof RemotePluginConfig] !== false;\n}\n"],"mappings":"qJASA,IAAI,aAA0C,KAE9C,eAAsB,2BAA2C,CAC/D,GAAI,CAAC,kBAAkB,EACrB,OAGF,IAAM,IAAM,uBAAuB,EAC/B,OAAI,YAAc,KAItB,GAAI,CACF,IAAM,IAAM,cACV,GAAG,IAAI,SAAS,UAAU,SAC1B,IAAI,SACN,EACM,SAAW,MAAM,MAAM,IAAK,CAChC,OAAQ,MACR,QAAS,CACP,eAAgB,kBAClB,EACA,OAAQ,YAAY,QAAQ,GAAM,CACpC,CAAC,EAED,GAAI,CAAC,SAAS,GACZ,OAGF,IAAM,OAAU,MAAM,SAAS,KAAK,EAChC,QAAQ,UACV,aAAe,OAAO,QAE1B,MAAQ,CAER,CACF,CAEA,SAAgB,gBAAgB,OAAyB,CAIvD,OAHK,aAGE,aAAa,UAAwC,GAFnD,EAGX"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/internal/server/trace-meta.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Server Component that emits a `<meta name="traceparent">` tag so
|
|
4
|
+
* the client SDK can stitch every browser span onto the server-side
|
|
5
|
+
* trace.
|
|
6
|
+
*
|
|
7
|
+
* Customer usage — drop into the root layout's `<head>` (TanStack
|
|
8
|
+
* Start uses the `head()` route option, but the meta tag also works
|
|
9
|
+
* if rendered into the React tree):
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { TraceMeta } from "@interfere/vite/server";
|
|
13
|
+
*
|
|
14
|
+
* function RootComponent() {
|
|
15
|
+
* return (
|
|
16
|
+
* <html>
|
|
17
|
+
* <head>
|
|
18
|
+
* <TraceMeta />
|
|
19
|
+
* </head>
|
|
20
|
+
* <body>...</body>
|
|
21
|
+
* </html>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* Renders nothing when no OTel span is active (dev without OTel
|
|
27
|
+
* registered, or unsampled traces). The client SDK's propagation
|
|
28
|
+
* reader handles a missing meta tag the same as no parent — every
|
|
29
|
+
* browser span starts a fresh root, which is at least internally
|
|
30
|
+
* consistent.
|
|
31
|
+
*/
|
|
32
|
+
declare function TraceMeta(): import("react/jsx-runtime").JSX.Element | null;
|
|
33
|
+
//#endregion
|
|
34
|
+
export { TraceMeta };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-meta.d.mts","names":[],"sources":["../../../src/internal/server/trace-meta.tsx"],"mappings":";;AAgCA;;;;AAAyB;;;;;;;;;;;;;;;;;;;;;;;;;iBAAT,SAAA,CAAA,+BAAS,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{activeTraceparent}from"./traceparent.mjs";import{jsx}from"react/jsx-runtime";function TraceMeta(){let traceparent=activeTraceparent();return traceparent?jsx(`meta`,{content:traceparent,name:`traceparent`}):null}export{TraceMeta};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-meta.mjs","names":[],"sources":["../../../src/internal/server/trace-meta.tsx"],"sourcesContent":["import { activeTraceparent } from \"./traceparent.js\";\n\n/**\n * Server Component that emits a `<meta name=\"traceparent\">` tag so\n * the client SDK can stitch every browser span onto the server-side\n * trace.\n *\n * Customer usage — drop into the root layout's `<head>` (TanStack\n * Start uses the `head()` route option, but the meta tag also works\n * if rendered into the React tree):\n *\n * ```tsx\n * import { TraceMeta } from \"@interfere/vite/server\";\n *\n * function RootComponent() {\n * return (\n * <html>\n * <head>\n * <TraceMeta />\n * </head>\n * <body>...</body>\n * </html>\n * );\n * }\n * ```\n *\n * Renders nothing when no OTel span is active (dev without OTel\n * registered, or unsampled traces). The client SDK's propagation\n * reader handles a missing meta tag the same as no parent — every\n * browser span starts a fresh root, which is at least internally\n * consistent.\n */\nexport function TraceMeta() {\n const traceparent = activeTraceparent();\n if (!traceparent) {\n return null;\n }\n return <meta content={traceparent} name=\"traceparent\" />;\n}\n"],"mappings":"oFAgCA,SAAgB,WAAY,CAC1B,IAAM,YAAc,kBAAkB,EAItC,OAHK,YAGE,IAAC,OAAD,CAAM,QAAS,YAAa,KAAK,aAAe,CAAA,EAF9C,IAGX"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/internal/server/traceparent.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build a W3C `traceparent` string from the currently-active OTel
|
|
4
|
+
* span, or `null` if no span is active or the trace is not sampled.
|
|
5
|
+
*
|
|
6
|
+
* Server-side counterpart to the client SDK's
|
|
7
|
+
* `internal/otel/propagation.ts`: both ends of the wire filter
|
|
8
|
+
* unsampled traces. The browser doesn't honor the `sampled` bit
|
|
9
|
+
* when creating child spans, so propagating an unsampled trace_id
|
|
10
|
+
* from the server would orphan every browser span under a trace
|
|
11
|
+
* with no recorded server segments. Returning null lets the
|
|
12
|
+
* browser fall back to its own root, which is at least internally
|
|
13
|
+
* consistent.
|
|
14
|
+
*/
|
|
15
|
+
declare function activeTraceparent(): string | null;
|
|
16
|
+
//#endregion
|
|
17
|
+
export { activeTraceparent };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traceparent.d.mts","names":[],"sources":["../../../src/internal/server/traceparent.ts"],"mappings":";;AAkBA;;;;AAAiC;;;;;;;;iBAAjB,iBAAA,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{TraceFlags,trace}from"@opentelemetry/api";const SAMPLED_BIT=TraceFlags.SAMPLED;function activeTraceparent(){let span=trace.getActiveSpan();if(!span)return null;let ctx=span.spanContext();if((ctx.traceFlags&SAMPLED_BIT)!==SAMPLED_BIT)return null;let flags=ctx.traceFlags.toString(16).padStart(2,`0`);return`00-${ctx.traceId}-${ctx.spanId}-${flags}`}export{activeTraceparent};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"traceparent.mjs","names":[],"sources":["../../../src/internal/server/traceparent.ts"],"sourcesContent":["import { TraceFlags, trace } from \"@opentelemetry/api\";\n\nconst SAMPLED_BIT = TraceFlags.SAMPLED;\nconst W3C_TRACECONTEXT_VERSION = \"00\";\n\n/**\n * Build a W3C `traceparent` string from the currently-active OTel\n * span, or `null` if no span is active or the trace is not sampled.\n *\n * Server-side counterpart to the client SDK's\n * `internal/otel/propagation.ts`: both ends of the wire filter\n * unsampled traces. The browser doesn't honor the `sampled` bit\n * when creating child spans, so propagating an unsampled trace_id\n * from the server would orphan every browser span under a trace\n * with no recorded server segments. Returning null lets the\n * browser fall back to its own root, which is at least internally\n * consistent.\n */\nexport function activeTraceparent(): string | null {\n const span = trace.getActiveSpan();\n if (!span) {\n return null;\n }\n const ctx = span.spanContext();\n // biome-ignore lint/suspicious/noBitwiseOperators: W3C trace_flags is a bitmask; SAMPLED (0x01) must be tested independently of any future reserved flags.\n if ((ctx.traceFlags & SAMPLED_BIT) !== SAMPLED_BIT) {\n return null;\n }\n const flags = ctx.traceFlags.toString(16).padStart(2, \"0\");\n return `${W3C_TRACECONTEXT_VERSION}-${ctx.traceId}-${ctx.spanId}-${flags}`;\n}\n"],"mappings":"iDAEA,MAAM,YAAc,WAAW,QAgB/B,SAAgB,mBAAmC,CACjD,IAAM,KAAO,MAAM,cAAc,EACjC,GAAI,CAAC,KACH,OAAO,KAET,IAAM,IAAM,KAAK,YAAY,EAE7B,IAAK,IAAI,WAAa,eAAiB,YACrC,OAAO,KAET,IAAM,MAAQ,IAAI,WAAW,SAAS,EAAE,EAAE,SAAS,EAAG,GAAG,EACzD,MAAO,MAA+B,IAAI,QAAQ,GAAG,IAAI,OAAO,GAAG,OACrE"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ErrorMechanism } from "@interfere/types/sdk/plugins/payload/errors";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/server/types.d.ts
|
|
4
|
+
interface CaptureErrorContext {
|
|
5
|
+
readonly mechanism?: ErrorMechanism;
|
|
6
|
+
/**
|
|
7
|
+
* Optional request method / path for the captured error. Stamped
|
|
8
|
+
* onto the OTel exception event as semconv `http.request.method`
|
|
9
|
+
* / `url.path` so dashboards can slice server errors by route.
|
|
10
|
+
*/
|
|
11
|
+
readonly request?: {
|
|
12
|
+
method?: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { CaptureErrorContext };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../src/internal/server/types.ts"],"mappings":";;;UAEiB,mBAAA;EAAA,SACN,SAAA,GAAY,cAAc;EADpB;;;;;EAAA,SAON,OAAA;IACP,MAAA;IACA,IAAA;EAAA;AAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export{};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.d.mts","names":[],"sources":["../../../src/internal/server/url.ts"],"mappings":";iBAEgB,aAAA,CAAc,GAAA,UAAa,SAAiB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function withPublicKey(url,publicKey){let next=new URL(url);return next.searchParams.set(`pk`,publicKey),next.toString()}export{withPublicKey};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url.mjs","names":[],"sources":["../../../src/internal/server/url.ts"],"sourcesContent":["const PUBLIC_KEY_QUERY = \"pk\";\n\nexport function withPublicKey(url: string, publicKey: string): string {\n const next = new URL(url);\n next.searchParams.set(PUBLIC_KEY_QUERY, publicKey);\n return next.toString();\n}\n"],"mappings":"AAEA,SAAgB,cAAc,IAAa,UAA2B,CACpE,IAAM,KAAO,IAAI,IAAI,GAAG,EAExB,OADA,KAAK,aAAa,IAAI,KAAkB,SAAS,EAC1C,KAAK,SAAS,CACvB"}
|
package/dist/package.mjs
CHANGED
package/dist/plugin.d.mts
CHANGED
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
import { Plugin } from "vite";
|
|
2
2
|
|
|
3
3
|
//#region src/plugin.d.ts
|
|
4
|
+
type InterfereViteMode = "auto" | "direct" | "proxy";
|
|
4
5
|
interface InterfereViteOptions {
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Override the commit SHA used to derive the release slug. Defaults
|
|
8
|
+
* to `VITE_INTERFERE_COMMIT_SHA` env then `git rev-parse HEAD`.
|
|
9
|
+
* Set this when the build runs outside a git checkout (e.g. a
|
|
10
|
+
* Docker layer-cached image build).
|
|
11
|
+
*/
|
|
12
|
+
commitSha?: string;
|
|
13
|
+
/**
|
|
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.
|
|
29
|
+
*/
|
|
30
|
+
mode?: InterfereViteMode;
|
|
31
|
+
/**
|
|
32
|
+
* `false` skips the post-build release metadata pipeline. Defaults
|
|
33
|
+
* to enabled when an `INTERFERE_API_KEY` is set; defaults to
|
|
34
|
+
* disabled when one isn't (so dev runs without creds don't fail).
|
|
35
|
+
* Useful when source maps are uploaded out-of-band via the
|
|
36
|
+
* `@interfere/cli`.
|
|
37
|
+
*/
|
|
38
|
+
sourceMaps?: boolean;
|
|
7
39
|
}
|
|
8
40
|
declare function interfere(options?: InterfereViteOptions): Plugin;
|
|
9
41
|
//#endregion
|
|
10
|
-
export { InterfereViteOptions, interfere as default, interfere };
|
|
42
|
+
export { InterfereViteMode, InterfereViteOptions, interfere as default, interfere };
|
package/dist/plugin.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"plugin.d.mts","names":[],"sources":["../src/plugin.ts"],"mappings":";;;KAwBY,iBAAA;AAAA,UAEK,oBAAA;EAFL;;;;AAAiB;AAE7B;EAOE,SAAA;;;;;;;;AA0BU;AA8WZ;;;;;;;;AAAqE;EAtXnE,IAAA,GAAO,iBAAiB;;;;;;;;EAQxB,UAAA;AAAA;AAAA,iBA8Wc,SAAA,CAAU,OAAA,GAAS,oBAAA,GAA4B,MAAM"}
|
package/dist/plugin.mjs
CHANGED
|
@@ -1,34 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
function
|
|
4
|
-
try {
|
|
5
|
-
return execSync("git rev-parse HEAD", { encoding: "utf8" }).trim();
|
|
6
|
-
} catch {
|
|
7
|
-
return null;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
function interfere(options) {
|
|
11
|
-
let buildId;
|
|
12
|
-
let releaseId;
|
|
13
|
-
let publicKey;
|
|
14
|
-
return {
|
|
15
|
-
name: "interfere",
|
|
16
|
-
enforce: "pre",
|
|
17
|
-
configResolved(config) {
|
|
18
|
-
buildId = options?.buildId ?? config.env["VITE_INTERFERE_BUILD_ID"] ?? resolveGitSha() ?? crypto.randomUUID();
|
|
19
|
-
releaseId = options?.releaseId ?? buildId;
|
|
20
|
-
publicKey = config.env["VITE_INTERFERE_PUBLIC_KEY"];
|
|
21
|
-
},
|
|
22
|
-
transformIndexHtml() {
|
|
23
|
-
const assignments = [`globalThis["__INTERFERE_BUILD_ID__"]=${JSON.stringify(buildId)};`, `globalThis["__INTERFERE_RELEASE_ID__"]=${JSON.stringify(releaseId)};`];
|
|
24
|
-
if (publicKey) assignments.push(`globalThis["__INTERFERE_PUBLIC_KEY__"]=${JSON.stringify(publicKey)};`);
|
|
25
|
-
return [{
|
|
26
|
-
tag: "script",
|
|
27
|
-
children: assignments.join(""),
|
|
28
|
-
injectTo: "head-prepend"
|
|
29
|
-
}];
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
//#endregion
|
|
34
|
-
export { interfere as default, interfere };
|
|
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{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`];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 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(`
|
|
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){let requested=optsMode??`auto`;if(requested!==`auto`)return logger.info(`[interfere] mode=${requested} (explicit). Plugin will ${requested===`direct`?`stamp the public key for direct ingestion`:`stamp the public key and run the SDK in proxy mode`}.`),requested;let detected=detectServerRuntime(config)?`proxy`:`direct`;return 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.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;logger.info(`[interfere] uploading source maps for release ${releaseSlug}…`);try{let{sdk}=createSdkClient({apiKey,apiUrl}),[discovered,releasesConfig]=await Promise.all([discoverByPairing({projectDir,distDir:uploadOutDir,subtrees:[`.`],publicPathRewrites:[{from:`public`,to:``}]}),sdk.releases.getConfig()]);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,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);logger.warn(`[interfere] release pipeline failed: ${message}. Build continues; events from this release will ingest but be ignored as evidence.`)}}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 buildPluginState(options,config){let logger=config.logger,mode=resolveMode(options.mode,config,logger),publicKey=config.env.VITE_INTERFERE_PUBLIC_KEY;config.command===`build`&&!publicKey&&failMissingPublicKey();let commitSha=options.commitSha??config.env.VITE_INTERFERE_COMMIT_SHA??resolveCommitSha(),releaseSlug=commitSha?deriveReleaseSlug(commitSha):null,fileEnv=loadEnv(config.mode,config.envDir,``),pickEnv=key=>{let shell=process.env[key];if(shell&&shell.length>0)return shell;let file=fileEnv[key];return file&&file.length>0?file:void 0},apiKey=pickEnv(`INTERFERE_API_KEY`),apiUrl=pickEnv(`INTERFERE_API_URL`)??API_URL,enableUpload=options.sourceMaps??(apiKey!==void 0&&apiKey.length>0&&releaseSlug!==null),rawBrowserApiUrl=config.env.VITE_INTERFERE_API_URL,browserApiUrl=rawBrowserApiUrl&&rawBrowserApiUrl.length>0?rawBrowserApiUrl:void 0;return emitSetupWarnings({apiKey,isProductionBuild:config.command===`build`,logger,mode,publicKeyPresent:!!publicKey,releaseSlug,sourceMapsExplicit:options.sourceMaps}),{apiKey,apiUrl,browserApiUrl:mode===`direct`?browserApiUrl:void 0,commitSha,enableUpload,forceEnable:isTruthyEnvFlag(config.env.VITE_INTERFERE_FORCE_ENABLE),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;return{name:`interfere`,enforce:`pre`,configResolved(config){let next=buildPluginState(options,config);state?.mode===`proxy`&&next.mode===`proxy`&&(next.uploadOutDir=state.uploadOutDir,next.uploadStarted=state.uploadStarted,next.buildStartedAt=state.buildStartedAt),state=next},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?.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 as default,interfere};
|