@interfere/next 0.0.14 → 0.0.15-alpha.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 +1 -6
- package/dist/build/env-config.d.mts +7 -0
- package/dist/build/env-config.d.mts.map +1 -0
- package/dist/build/env-config.mjs +17 -0
- package/dist/build/env-config.mjs.map +1 -0
- package/dist/build/logger.d.mts +11 -0
- package/dist/build/logger.d.mts.map +1 -0
- package/dist/build/logger.mjs +155 -0
- package/dist/build/logger.mjs.map +1 -0
- package/dist/build/release-program.d.mts +19 -0
- package/dist/build/release-program.d.mts.map +1 -0
- package/dist/build/release-program.mjs +92 -0
- package/dist/build/release-program.mjs.map +1 -0
- package/dist/build/secret-key.d.mts +10 -0
- package/dist/build/secret-key.d.mts.map +1 -0
- package/dist/build/secret-key.mjs +16 -0
- package/dist/build/secret-key.mjs.map +1 -0
- package/dist/build/services/config.service.d.mts +9 -0
- package/dist/build/services/config.service.d.mts.map +1 -0
- package/dist/build/services/config.service.mjs +8 -0
- package/dist/build/services/config.service.mjs.map +1 -0
- package/dist/build/services/preflight.service.d.mts +19 -0
- package/dist/build/services/preflight.service.d.mts.map +1 -0
- package/dist/build/services/preflight.service.mjs +76 -0
- package/dist/build/services/preflight.service.mjs.map +1 -0
- package/dist/build/services/release-identity.service.d.mts +22 -0
- package/dist/build/services/release-identity.service.d.mts.map +1 -0
- package/dist/build/services/release-identity.service.mjs +48 -0
- package/dist/build/services/release-identity.service.mjs.map +1 -0
- package/dist/build/services/source-map.service.d.mts +24 -0
- package/dist/build/services/source-map.service.d.mts.map +1 -0
- package/dist/build/services/source-map.service.mjs +58 -0
- package/dist/build/services/source-map.service.mjs.map +1 -0
- package/dist/build/source-maps/api.d.mts +35 -0
- package/dist/build/source-maps/api.d.mts.map +1 -0
- package/dist/build/source-maps/api.mjs +61 -0
- package/dist/build/source-maps/api.mjs.map +1 -0
- package/dist/build/source-maps/client.d.mts +73 -0
- package/dist/build/source-maps/client.d.mts.map +1 -0
- package/dist/build/source-maps/client.mjs +228 -0
- package/dist/build/source-maps/client.mjs.map +1 -0
- package/dist/build/source-maps/errors.d.mts +109 -0
- package/dist/build/source-maps/errors.d.mts.map +1 -0
- package/dist/build/source-maps/errors.mjs +22 -0
- package/dist/build/source-maps/errors.mjs.map +1 -0
- package/dist/build/source-maps/files.d.mts +35 -0
- package/dist/build/source-maps/files.d.mts.map +1 -0
- package/dist/build/source-maps/files.mjs +222 -0
- package/dist/build/source-maps/files.mjs.map +1 -0
- package/dist/build/source-maps/providers/deployment/detector.d.mts +26 -0
- package/dist/build/source-maps/providers/deployment/detector.d.mts.map +1 -0
- package/dist/build/source-maps/providers/deployment/detector.mjs +22 -0
- package/dist/build/source-maps/providers/deployment/detector.mjs.map +1 -0
- package/dist/build/source-maps/providers/deployment/types.d.mts +12 -0
- package/dist/build/source-maps/providers/deployment/types.d.mts.map +1 -0
- package/dist/build/source-maps/providers/deployment/types.mjs +3 -0
- package/dist/build/source-maps/providers/deployment/vercel.d.mts +6 -0
- package/dist/build/source-maps/providers/deployment/vercel.d.mts.map +1 -0
- package/dist/build/source-maps/providers/deployment/vercel.mjs +44 -0
- package/dist/build/source-maps/providers/deployment/vercel.mjs.map +1 -0
- package/dist/build/source-maps/providers/source-control/detector.d.mts +15 -0
- package/dist/build/source-maps/providers/source-control/detector.d.mts.map +1 -0
- package/dist/build/source-maps/providers/source-control/detector.mjs +22 -0
- package/dist/build/source-maps/providers/source-control/detector.mjs.map +1 -0
- package/dist/build/source-maps/providers/source-control/git.d.mts +6 -0
- package/dist/build/source-maps/providers/source-control/git.d.mts.map +1 -0
- package/dist/build/source-maps/providers/source-control/git.mjs +50 -0
- package/dist/build/source-maps/providers/source-control/git.mjs.map +1 -0
- package/dist/build/source-maps/providers/source-control/types.d.mts +12 -0
- package/dist/build/source-maps/providers/source-control/types.d.mts.map +1 -0
- package/dist/build/source-maps/providers/source-control/types.mjs +3 -0
- package/dist/build/with-interfere.d.mts +49 -0
- package/dist/build/with-interfere.d.mts.map +1 -0
- package/dist/build/with-interfere.mjs +75 -0
- package/dist/build/with-interfere.mjs.map +1 -0
- package/dist/client/client.d.mts +3 -0
- package/dist/client/client.mjs +5 -0
- package/dist/client/provider.d.mts +22 -0
- package/dist/client/provider.d.mts.map +1 -0
- package/dist/client/provider.mjs +33 -0
- package/dist/client/provider.mjs.map +1 -0
- package/dist/lib/env.d.mts +12 -0
- package/dist/lib/env.d.mts.map +1 -0
- package/dist/lib/env.mjs +17 -0
- package/dist/lib/env.mjs.map +1 -0
- package/dist/lib/test-utils/make-next-request.d.mts +6 -0
- package/dist/lib/test-utils/make-next-request.d.mts.map +1 -0
- package/dist/lib/test-utils/make-next-request.mjs +12 -0
- package/dist/lib/test-utils/make-next-request.mjs.map +1 -0
- package/dist/lib/types.d.mts +22 -0
- package/dist/lib/types.d.mts.map +1 -0
- package/dist/lib/types.mjs +7 -0
- package/dist/lib/types.mjs.map +1 -0
- package/dist/server/middleware.d.mts +11 -0
- package/dist/server/middleware.d.mts.map +1 -0
- package/dist/server/middleware.mjs +84 -0
- package/dist/server/middleware.mjs.map +1 -0
- package/dist/server/proxy.d.mts +6 -0
- package/dist/server/proxy.d.mts.map +1 -0
- package/dist/server/proxy.mjs +29 -0
- package/dist/server/proxy.mjs.map +1 -0
- package/dist/server/route-handler.d.mts +9 -0
- package/dist/server/route-handler.d.mts.map +1 -0
- package/dist/server/route-handler.mjs +172 -0
- package/dist/server/route-handler.mjs.map +1 -0
- package/dist/server/services/config.service.d.mts +21 -0
- package/dist/server/services/config.service.d.mts.map +1 -0
- package/dist/server/services/config.service.mjs +43 -0
- package/dist/server/services/config.service.mjs.map +1 -0
- package/dist/server/services/error-tracking.service.d.mts +19 -0
- package/dist/server/services/error-tracking.service.d.mts.map +1 -0
- package/dist/server/services/error-tracking.service.mjs +31 -0
- package/dist/server/services/error-tracking.service.mjs.map +1 -0
- package/package.json +67 -36
- package/dist/__tests__/build/with-interfere-coverage.test.d.ts +0 -2
- package/dist/__tests__/build/with-interfere-coverage.test.d.ts.map +0 -1
- package/dist/__tests__/build/with-interfere-coverage.test.js +0 -295
- package/dist/__tests__/build/with-interfere-coverage.test.js.map +0 -1
- package/dist/__tests__/build/with-interfere.test.d.ts +0 -2
- package/dist/__tests__/build/with-interfere.test.d.ts.map +0 -1
- package/dist/__tests__/build/with-interfere.test.js +0 -363
- package/dist/__tests__/build/with-interfere.test.js.map +0 -1
- package/dist/__tests__/core/client.test.d.ts +0 -2
- package/dist/__tests__/core/client.test.d.ts.map +0 -1
- package/dist/__tests__/core/client.test.js +0 -373
- package/dist/__tests__/core/client.test.js.map +0 -1
- package/dist/__tests__/core/encoders.test.d.ts +0 -2
- package/dist/__tests__/core/encoders.test.d.ts.map +0 -1
- package/dist/__tests__/core/encoders.test.js +0 -56
- package/dist/__tests__/core/encoders.test.js.map +0 -1
- package/dist/__tests__/core/rage-click.test.d.ts +0 -2
- package/dist/__tests__/core/rage-click.test.d.ts.map +0 -1
- package/dist/__tests__/core/rage-click.test.js +0 -121
- package/dist/__tests__/core/rage-click.test.js.map +0 -1
- package/dist/__tests__/core/session-manager.test.d.ts +0 -2
- package/dist/__tests__/core/session-manager.test.d.ts.map +0 -1
- package/dist/__tests__/core/session-manager.test.js +0 -1168
- package/dist/__tests__/core/session-manager.test.js.map +0 -1
- package/dist/__tests__/integration/release-upload.test.d.ts +0 -2
- package/dist/__tests__/integration/release-upload.test.d.ts.map +0 -1
- package/dist/__tests__/integration/release-upload.test.js +0 -153
- package/dist/__tests__/integration/release-upload.test.js.map +0 -1
- package/dist/__tests__/provider.test.d.ts +0 -2
- package/dist/__tests__/provider.test.d.ts.map +0 -1
- package/dist/__tests__/provider.test.js +0 -84
- package/dist/__tests__/provider.test.js.map +0 -1
- package/dist/__tests__/session/persistence.test.d.ts +0 -2
- package/dist/__tests__/session/persistence.test.d.ts.map +0 -1
- package/dist/__tests__/session/persistence.test.js +0 -129
- package/dist/__tests__/session/persistence.test.js.map +0 -1
- package/dist/__tests__/session/session-summary.test.d.ts +0 -2
- package/dist/__tests__/session/session-summary.test.d.ts.map +0 -1
- package/dist/__tests__/session/session-summary.test.js +0 -763
- package/dist/__tests__/session/session-summary.test.js.map +0 -1
- package/dist/client.d.ts +0 -75
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -123
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -40
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -340
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts +0 -37
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -49
- package/dist/index.js.map +0 -1
- package/dist/index.jsx +0 -87
- package/dist/index.jsx.map +0 -1
- package/dist/lib/core/client-core.d.ts +0 -27
- package/dist/lib/core/client-core.d.ts.map +0 -1
- package/dist/lib/core/client-core.js +0 -152
- package/dist/lib/core/client-core.js.map +0 -1
- package/dist/lib/core/constants.d.ts +0 -12
- package/dist/lib/core/constants.d.ts.map +0 -1
- package/dist/lib/core/constants.js +0 -17
- package/dist/lib/core/constants.js.map +0 -1
- package/dist/lib/core/debug.d.ts +0 -47
- package/dist/lib/core/debug.d.ts.map +0 -1
- package/dist/lib/core/debug.js +0 -79
- package/dist/lib/core/debug.js.map +0 -1
- package/dist/lib/core/encoders.d.ts +0 -3
- package/dist/lib/core/encoders.d.ts.map +0 -1
- package/dist/lib/core/encoders.js +0 -5
- package/dist/lib/core/encoders.js.map +0 -1
- package/dist/lib/core/error-handlers.d.ts +0 -14
- package/dist/lib/core/error-handlers.d.ts.map +0 -1
- package/dist/lib/core/error-handlers.js +0 -191
- package/dist/lib/core/error-handlers.js.map +0 -1
- package/dist/lib/core/runtime.d.ts +0 -7
- package/dist/lib/core/runtime.d.ts.map +0 -1
- package/dist/lib/core/runtime.js +0 -16
- package/dist/lib/core/runtime.js.map +0 -1
- package/dist/lib/persistence/storage.d.ts +0 -5
- package/dist/lib/persistence/storage.d.ts.map +0 -1
- package/dist/lib/persistence/storage.js +0 -67
- package/dist/lib/persistence/storage.js.map +0 -1
- package/dist/lib/session/constants.d.ts +0 -19
- package/dist/lib/session/constants.d.ts.map +0 -1
- package/dist/lib/session/constants.js +0 -34
- package/dist/lib/session/constants.js.map +0 -1
- package/dist/lib/session/persistence.d.ts +0 -58
- package/dist/lib/session/persistence.d.ts.map +0 -1
- package/dist/lib/session/persistence.js +0 -179
- package/dist/lib/session/persistence.js.map +0 -1
- package/dist/lib/session/rage-click.d.ts +0 -17
- package/dist/lib/session/rage-click.d.ts.map +0 -1
- package/dist/lib/session/rage-click.js +0 -104
- package/dist/lib/session/rage-click.js.map +0 -1
- package/dist/lib/session/replay.d.ts +0 -3
- package/dist/lib/session/replay.d.ts.map +0 -1
- package/dist/lib/session/replay.js +0 -109
- package/dist/lib/session/replay.js.map +0 -1
- package/dist/lib/session/session-manager.d.ts +0 -126
- package/dist/lib/session/session-manager.d.ts.map +0 -1
- package/dist/lib/session/session-manager.js +0 -635
- package/dist/lib/session/session-manager.js.map +0 -1
- package/dist/lib/session/session-summary.d.ts +0 -3
- package/dist/lib/session/session-summary.d.ts.map +0 -1
- package/dist/lib/session/session-summary.js +0 -214
- package/dist/lib/session/session-summary.js.map +0 -1
- package/dist/middleware.d.ts +0 -8
- package/dist/middleware.d.ts.map +0 -1
- package/dist/middleware.js +0 -139
- package/dist/middleware.js.map +0 -1
- package/dist/types/storage.d.ts +0 -7
- package/dist/types/storage.d.ts.map +0 -1
- package/dist/types/storage.js +0 -2
- package/dist/types/storage.js.map +0 -1
- package/dist/types.d.ts +0 -6
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -4
- package/dist/types.js.map +0 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"use server";
|
|
2
|
+
|
|
3
|
+
import { ConfigService, ConfigServiceLive } from "./services/config.service.mjs";
|
|
4
|
+
import { ErrorTrackingService, ErrorTrackingServiceLive } from "./services/error-tracking.service.mjs";
|
|
5
|
+
import { API_PATHS } from "@interfere/constants/api";
|
|
6
|
+
import { withInterfereLogger } from "@interfere/effect-utils/observability";
|
|
7
|
+
import { Data, Effect, Layer, Schedule } from "effect";
|
|
8
|
+
import { withSpan } from "@interfere/react/effect/layers/tracer.layer";
|
|
9
|
+
import { createBackoffSchedule, shouldRetryHttp, withTimeoutFail } from "@interfere/effect-utils/retry";
|
|
10
|
+
import { envelopeSchema } from "@interfere/types/sdk/envelope";
|
|
11
|
+
import { v7 } from "uuid";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
//#region src/server/route-handler.ts
|
|
15
|
+
/** biome-ignore-all lint/suspicious/useAwait: route handlers must be async */
|
|
16
|
+
const layer = Layer.mergeAll(ConfigServiceLive, ErrorTrackingServiceLive);
|
|
17
|
+
var ProxyError = class extends Data.TaggedError("ProxyError") {};
|
|
18
|
+
const TIMEOUT_MS = 1e4;
|
|
19
|
+
const RETRY_COUNT = 2;
|
|
20
|
+
const HTTP_BAD_GATEWAY = 502;
|
|
21
|
+
const RETRY_INITIAL_MS = 100;
|
|
22
|
+
const RETRY_MAX_MS = 2e3;
|
|
23
|
+
const envelopesArraySchema = z.array(envelopeSchema);
|
|
24
|
+
const GET = async () => {
|
|
25
|
+
const program = Effect.gen(function* () {
|
|
26
|
+
const configured = yield* (yield* ConfigService).getSecretKey.pipe(Effect.map(() => true), Effect.catchAll(() => Effect.succeed(false)));
|
|
27
|
+
return Response.json({
|
|
28
|
+
status: "ok",
|
|
29
|
+
configured,
|
|
30
|
+
timestamp: Date.now()
|
|
31
|
+
});
|
|
32
|
+
}).pipe(Effect.provide(layer));
|
|
33
|
+
return await Effect.runPromise(withInterfereLogger("next", program));
|
|
34
|
+
};
|
|
35
|
+
const proxyIngestRequest = (request, params) => Effect.gen(function* () {
|
|
36
|
+
const traceparent = (request.headers.get("traceparent") || void 0) ?? generateTraceparent();
|
|
37
|
+
const body = yield* Effect.tryPromise({
|
|
38
|
+
try: () => request.clone().text(),
|
|
39
|
+
catch: () => new ProxyError({
|
|
40
|
+
status: 400,
|
|
41
|
+
message: "Invalid request body"
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(body);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
yield* Effect.logWarning("Invalid JSON in request body").pipe(Effect.annotateLogs({ error: String(error) }));
|
|
49
|
+
return yield* Effect.fail(new ProxyError({
|
|
50
|
+
status: 400,
|
|
51
|
+
message: "Invalid JSON in request body",
|
|
52
|
+
body: String(error)
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
const validation = envelopesArraySchema.safeParse(parsed);
|
|
56
|
+
if (!validation.success) {
|
|
57
|
+
const issues = validation.error.issues.map((issue) => ({
|
|
58
|
+
path: issue.path.join("."),
|
|
59
|
+
message: issue.message,
|
|
60
|
+
code: issue.code
|
|
61
|
+
}));
|
|
62
|
+
yield* Effect.logWarning("Invalid envelope schema").pipe(Effect.annotateLogs({ validationErrors: issues }));
|
|
63
|
+
const b = JSON.stringify({
|
|
64
|
+
ok: false,
|
|
65
|
+
code: "INVALID_ENVELOPE_SCHEMA",
|
|
66
|
+
errors: issues
|
|
67
|
+
});
|
|
68
|
+
return new Response(b, {
|
|
69
|
+
status: 422,
|
|
70
|
+
headers: { "content-type": "application/json" }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const seen = /* @__PURE__ */ new Set();
|
|
74
|
+
const deduped = validation.data.filter((env) => {
|
|
75
|
+
if (seen.has(env.uuid)) return false;
|
|
76
|
+
seen.add(env.uuid);
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
const dropped = validation.data.length - deduped.length;
|
|
80
|
+
yield* Effect.logDebug("Envelope validation successful").pipe(Effect.annotateLogs({
|
|
81
|
+
envelopeCount: validation.data.length,
|
|
82
|
+
dedupedCount: deduped.length,
|
|
83
|
+
droppedDuplicates: dropped
|
|
84
|
+
}));
|
|
85
|
+
const upstream = yield* withTimeoutFail(Effect.tryPromise({
|
|
86
|
+
try: () => fetch(`${params.apiUrl}${API_PATHS.INGEST_V0}`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: {
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
"x-interfere-surface": params.surfaceToken,
|
|
91
|
+
traceparent
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify(deduped)
|
|
94
|
+
}),
|
|
95
|
+
catch: (error) => new ProxyError({
|
|
96
|
+
status: 0,
|
|
97
|
+
message: error instanceof Error ? error.message : String(error)
|
|
98
|
+
})
|
|
99
|
+
}), TIMEOUT_MS, () => new ProxyError({
|
|
100
|
+
status: 0,
|
|
101
|
+
message: "Network timeout"
|
|
102
|
+
}));
|
|
103
|
+
const responseBody = yield* Effect.tryPromise({
|
|
104
|
+
try: () => upstream.text(),
|
|
105
|
+
catch: () => ""
|
|
106
|
+
});
|
|
107
|
+
return new Response(responseBody, {
|
|
108
|
+
status: upstream.status,
|
|
109
|
+
headers: { "content-type": upstream.headers.get("content-type") || "application/json" }
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const POST = async (request) => {
|
|
113
|
+
const program = Effect.gen(function* () {
|
|
114
|
+
yield* Effect.logDebug("Handling ingest proxy request").pipe(Effect.annotateLogs({ path: request.nextUrl.pathname }));
|
|
115
|
+
const config = yield* ConfigService;
|
|
116
|
+
const errorTracking = yield* ErrorTrackingService;
|
|
117
|
+
yield* config.getSecretKey.pipe(Effect.tapError(() => Effect.logError("Service not configured - missing INTERFERE_SECRET_KEY")), Effect.catchTag("ConfigError", () => Effect.fail(new ProxyError({
|
|
118
|
+
status: 503,
|
|
119
|
+
message: "Service not configured. INTERFERE_SECRET_KEY environment variable is required."
|
|
120
|
+
}))));
|
|
121
|
+
const surfaceToken = yield* config.getPublicToken.pipe(Effect.tapError(() => Effect.logError("Auth not available - missing public token")), Effect.catchTag("ConfigError", () => Effect.fail(new ProxyError({
|
|
122
|
+
status: 503,
|
|
123
|
+
message: "Auth not available"
|
|
124
|
+
}))));
|
|
125
|
+
const apiUrl = yield* config.getApiUrl;
|
|
126
|
+
const schedule = createBackoffSchedule(RETRY_INITIAL_MS, RETRY_MAX_MS, RETRY_COUNT).pipe(Schedule.whileInput((e) => e instanceof ProxyError ? shouldRetryHttp(e.status) : false));
|
|
127
|
+
const response = yield* withSpan("proxy.forward", proxyIngestRequest(request, {
|
|
128
|
+
apiUrl,
|
|
129
|
+
surfaceToken
|
|
130
|
+
}), { attributes: {} }).pipe(Effect.retry(schedule), Effect.tapError((error) => Effect.gen(function* () {
|
|
131
|
+
yield* errorTracking.captureError(error, request, {
|
|
132
|
+
pathname: request.nextUrl.pathname,
|
|
133
|
+
type: "api_route"
|
|
134
|
+
});
|
|
135
|
+
yield* Effect.logError("Proxy request failed").pipe(Effect.annotateLogs({
|
|
136
|
+
error: String(error),
|
|
137
|
+
status: error instanceof ProxyError ? error.status : 0
|
|
138
|
+
}));
|
|
139
|
+
})));
|
|
140
|
+
yield* Effect.logDebug("Proxy request completed").pipe(Effect.annotateLogs({ status: response.status }));
|
|
141
|
+
return response;
|
|
142
|
+
}).pipe(Effect.provide(layer), Effect.catchTag("ProxyError", (error) => Effect.succeed(Response.json({ error: error.message }, { status: error.status || HTTP_BAD_GATEWAY }))), Effect.catchAll((error) => {
|
|
143
|
+
Effect.runFork(Effect.logError("Unexpected error in route handler").pipe(Effect.annotateLogs({ error: String(error) })));
|
|
144
|
+
return Effect.succeed(Response.json({ error: "Internal server error" }, { status: 500 }));
|
|
145
|
+
}));
|
|
146
|
+
return await Effect.runPromise(withInterfereLogger("next", program));
|
|
147
|
+
};
|
|
148
|
+
const OPTIONS = async () => Effect.runPromise(withInterfereLogger("next", Effect.succeed(new Response(null, {
|
|
149
|
+
status: 200,
|
|
150
|
+
headers: {
|
|
151
|
+
"Access-Control-Allow-Origin": "*",
|
|
152
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
153
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
154
|
+
"Access-Control-Max-Age": "86400"
|
|
155
|
+
}
|
|
156
|
+
}))));
|
|
157
|
+
const VERSION = "00";
|
|
158
|
+
const TRACE_ID_BYTES = 16;
|
|
159
|
+
const SPAN_ID_BYTES = 8;
|
|
160
|
+
const FLAGS = "01";
|
|
161
|
+
function generateTraceparent() {
|
|
162
|
+
return `${VERSION}-${randomHex(TRACE_ID_BYTES)}-${randomHex(SPAN_ID_BYTES)}-${FLAGS}`;
|
|
163
|
+
}
|
|
164
|
+
function randomHex(bytes) {
|
|
165
|
+
const raw = v7().replace(/-/g, "");
|
|
166
|
+
const needed = bytes * 2;
|
|
167
|
+
if (raw.length >= needed) return raw.substring(0, needed);
|
|
168
|
+
return (raw + raw).substring(0, needed);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
//#endregion
|
|
172
|
+
export { GET, OPTIONS, POST };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-handler.mjs","names":["GET: RouteHandler","parsed: unknown","POST: RouteHandler","OPTIONS: RouteHandler","uuidv7"],"sources":["../../src/server/route-handler.ts"],"sourcesContent":["/** biome-ignore-all lint/suspicious/useAwait: route handlers must be async */\n\"use server\";\n\nimport { API_PATHS } from \"@interfere/constants/api\";\nimport { withInterfereLogger } from \"@interfere/effect-utils/observability\";\nimport {\n createBackoffSchedule,\n shouldRetryHttp,\n withTimeoutFail,\n} from \"@interfere/effect-utils/retry\";\nimport { withSpan } from \"@interfere/react/effect/layers/tracer.layer\";\nimport { envelopeSchema } from \"@interfere/types/sdk/envelope\";\nimport { Data, Effect, Layer, Schedule } from \"effect\";\nimport type { NextRequest } from \"next/server\";\nimport { v7 as uuidv7 } from \"uuid\";\nimport { z } from \"zod\";\nimport { ConfigService, ConfigServiceLive } from \"./services/config.service\";\nimport {\n ErrorTrackingService,\n ErrorTrackingServiceLive,\n} from \"./services/error-tracking.service\";\n\ntype RouteHandler = (request: NextRequest) => Promise<Response>;\n\nconst layer = Layer.mergeAll(ConfigServiceLive, ErrorTrackingServiceLive);\n\n// Error types for the proxy\nclass ProxyError extends Data.TaggedError(\"ProxyError\")<{\n readonly status: number;\n readonly message: string;\n readonly body?: string;\n}> {}\n\nconst TIMEOUT_MS = 10_000;\nconst RETRY_COUNT = 2;\nconst HTTP_BAD_GATEWAY = 502;\nconst RETRY_INITIAL_MS = 100;\nconst RETRY_MAX_MS = 2000;\n\nconst envelopesArraySchema = z.array(envelopeSchema);\n\nexport const GET: RouteHandler = async () => {\n const program = Effect.gen(function* () {\n const config = yield* ConfigService;\n const configured = yield* config.getSecretKey.pipe(\n Effect.map(() => true),\n Effect.catchAll(() => Effect.succeed(false))\n );\n\n return Response.json({ status: \"ok\", configured, timestamp: Date.now() });\n }).pipe(Effect.provide(layer));\n\n return await Effect.runPromise(withInterfereLogger(\"next\", program));\n};\n\nconst proxyIngestRequest = (\n request: Request,\n params: { apiUrl: string; surfaceToken: string }\n) =>\n Effect.gen(function* () {\n const incomingTrace = request.headers.get(\"traceparent\") || undefined;\n const traceparent = incomingTrace ?? generateTraceparent();\n\n // Read request body once\n const body = yield* Effect.tryPromise({\n try: () => request.clone().text(),\n catch: () =>\n new ProxyError({ status: 400, message: \"Invalid request body\" }),\n });\n\n // Validate the envelopes before proxying\n let parsed: unknown;\n\n try {\n parsed = JSON.parse(body);\n } catch (error) {\n yield* Effect.logWarning(\"Invalid JSON in request body\").pipe(\n Effect.annotateLogs({ error: String(error) })\n );\n\n return yield* Effect.fail(\n new ProxyError({\n status: 400,\n message: \"Invalid JSON in request body\",\n body: String(error),\n })\n );\n }\n\n const validation = envelopesArraySchema.safeParse(parsed);\n\n if (!validation.success) {\n const issues = validation.error.issues.map((issue) => ({\n path: issue.path.join(\".\"),\n message: issue.message,\n code: issue.code,\n }));\n\n yield* Effect.logWarning(\"Invalid envelope schema\").pipe(\n Effect.annotateLogs({ validationErrors: issues })\n );\n\n // For malformed envelopes, short‑circuit and return a 422 directly.\n // This lets SDKs drop bad envelopes instead of retrying or feeding them\n // back into the error tracking pipeline.\n const b = JSON.stringify({\n ok: false,\n code: \"INVALID_ENVELOPE_SCHEMA\",\n errors: issues,\n });\n\n return new Response(b, {\n status: 422,\n headers: {\n \"content-type\": \"application/json\",\n },\n });\n }\n\n // De-duplicate by envelope.uuid to avoid reprocessing on client retries\n const seen = new Set<string>();\n\n const deduped = validation.data.filter((env) => {\n if (seen.has(env.uuid)) {\n return false;\n }\n\n seen.add(env.uuid);\n return true;\n });\n\n const dropped = validation.data.length - deduped.length;\n\n yield* Effect.logDebug(\"Envelope validation successful\").pipe(\n Effect.annotateLogs({\n envelopeCount: validation.data.length,\n dedupedCount: deduped.length,\n droppedDuplicates: dropped,\n })\n );\n\n const upstream = yield* withTimeoutFail(\n Effect.tryPromise({\n try: () =>\n fetch(`${params.apiUrl}${API_PATHS.INGEST_V0}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"x-interfere-surface\": params.surfaceToken,\n traceparent,\n },\n body: JSON.stringify(deduped),\n }),\n catch: (error) =>\n new ProxyError({\n status: 0,\n message: error instanceof Error ? error.message : String(error),\n }),\n }),\n TIMEOUT_MS,\n () => new ProxyError({ status: 0, message: \"Network timeout\" })\n );\n\n const responseBody = yield* Effect.tryPromise({\n try: () => upstream.text(),\n catch: () => \"\",\n });\n\n return new Response(responseBody, {\n status: upstream.status,\n headers: {\n \"content-type\":\n upstream.headers.get(\"content-type\") || \"application/json\",\n },\n });\n });\n\nexport const POST: RouteHandler = async (request): Promise<Response> => {\n const program = Effect.gen(function* () {\n yield* Effect.logDebug(\"Handling ingest proxy request\").pipe(\n Effect.annotateLogs({ path: request.nextUrl.pathname })\n );\n\n const config = yield* ConfigService;\n const errorTracking = yield* ErrorTrackingService;\n\n // Check secret key\n yield* config.getSecretKey.pipe(\n Effect.tapError(() =>\n Effect.logError(\"Service not configured - missing INTERFERE_SECRET_KEY\")\n ),\n Effect.catchTag(\"ConfigError\", () =>\n Effect.fail(\n new ProxyError({\n status: 503,\n message:\n \"Service not configured. INTERFERE_SECRET_KEY environment variable is required.\",\n })\n )\n )\n );\n\n // Get public token\n const surfaceToken = yield* config.getPublicToken.pipe(\n Effect.tapError(() =>\n Effect.logError(\"Auth not available - missing public token\")\n ),\n Effect.catchTag(\"ConfigError\", () =>\n Effect.fail(\n new ProxyError({\n status: 503,\n message: \"Auth not available\",\n })\n )\n )\n );\n\n const apiUrl = yield* config.getApiUrl;\n\n // Proxy the request with retry logic\n const schedule = createBackoffSchedule(\n RETRY_INITIAL_MS,\n RETRY_MAX_MS,\n RETRY_COUNT\n ).pipe(\n Schedule.whileInput((e: unknown) =>\n e instanceof ProxyError ? shouldRetryHttp(e.status) : false\n )\n );\n\n const response = yield* withSpan(\n \"proxy.forward\",\n proxyIngestRequest(request, { apiUrl, surfaceToken }),\n { attributes: {} }\n ).pipe(\n Effect.retry(schedule),\n Effect.tapError((error) =>\n Effect.gen(function* () {\n yield* errorTracking.captureError(error, request, {\n pathname: request.nextUrl.pathname,\n type: \"api_route\",\n });\n yield* Effect.logError(\"Proxy request failed\").pipe(\n Effect.annotateLogs({\n error: String(error),\n status: error instanceof ProxyError ? error.status : 0,\n })\n );\n })\n )\n );\n\n yield* Effect.logDebug(\"Proxy request completed\").pipe(\n Effect.annotateLogs({ status: response.status })\n );\n\n return response;\n }).pipe(\n Effect.provide(layer),\n Effect.catchTag(\"ProxyError\", (error) =>\n Effect.succeed(\n Response.json(\n { error: error.message },\n { status: error.status || HTTP_BAD_GATEWAY }\n )\n )\n ),\n Effect.catchAll((error) => {\n Effect.runFork(\n Effect.logError(\"Unexpected error in route handler\").pipe(\n Effect.annotateLogs({ error: String(error) })\n )\n );\n return Effect.succeed(\n Response.json({ error: \"Internal server error\" }, { status: 500 })\n );\n })\n );\n\n return await Effect.runPromise(withInterfereLogger(\"next\", program));\n};\n\nexport const OPTIONS: RouteHandler = async () =>\n Effect.runPromise(\n withInterfereLogger(\n \"next\",\n Effect.succeed(\n new Response(null, {\n status: 200,\n headers: {\n \"Access-Control-Allow-Origin\": \"*\",\n \"Access-Control-Allow-Methods\": \"GET, POST, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type\",\n \"Access-Control-Max-Age\": \"86400\",\n },\n })\n )\n )\n );\n\n// Helper functions\nconst VERSION = \"00\";\nconst TRACE_ID_BYTES = 16;\nconst SPAN_ID_BYTES = 8;\nconst FLAGS = \"01\";\n\nfunction generateTraceparent() {\n const traceId = randomHex(TRACE_ID_BYTES);\n const spanId = randomHex(SPAN_ID_BYTES);\n return `${VERSION}-${traceId}-${spanId}-${FLAGS}`;\n}\n\nfunction randomHex(bytes: number): string {\n const raw = uuidv7().replace(/-/g, \"\");\n const needed = bytes * 2;\n if (raw.length >= needed) {\n return raw.substring(0, needed);\n }\n return (raw + raw).substring(0, needed);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAwBA,MAAM,QAAQ,MAAM,SAAS,mBAAmB,yBAAyB;AAGzE,IAAM,aAAN,cAAyB,KAAK,YAAY,aAAa,CAIpD;AAEH,MAAM,aAAa;AACnB,MAAM,cAAc;AACpB,MAAM,mBAAmB;AACzB,MAAM,mBAAmB;AACzB,MAAM,eAAe;AAErB,MAAM,uBAAuB,EAAE,MAAM,eAAe;AAEpD,MAAaA,MAAoB,YAAY;CAC3C,MAAM,UAAU,OAAO,IAAI,aAAa;EAEtC,MAAM,aAAa,QADJ,OAAO,eACW,aAAa,KAC5C,OAAO,UAAU,KAAK,EACtB,OAAO,eAAe,OAAO,QAAQ,MAAM,CAAC,CAC7C;AAED,SAAO,SAAS,KAAK;GAAE,QAAQ;GAAM;GAAY,WAAW,KAAK,KAAK;GAAE,CAAC;GACzE,CAAC,KAAK,OAAO,QAAQ,MAAM,CAAC;AAE9B,QAAO,MAAM,OAAO,WAAW,oBAAoB,QAAQ,QAAQ,CAAC;;AAGtE,MAAM,sBACJ,SACA,WAEA,OAAO,IAAI,aAAa;CAEtB,MAAM,eADgB,QAAQ,QAAQ,IAAI,cAAc,IAAI,WACvB,qBAAqB;CAG1D,MAAM,OAAO,OAAO,OAAO,WAAW;EACpC,WAAW,QAAQ,OAAO,CAAC,MAAM;EACjC,aACE,IAAI,WAAW;GAAE,QAAQ;GAAK,SAAS;GAAwB,CAAC;EACnE,CAAC;CAGF,IAAIC;AAEJ,KAAI;AACF,WAAS,KAAK,MAAM,KAAK;UAClB,OAAO;AACd,SAAO,OAAO,WAAW,+BAA+B,CAAC,KACvD,OAAO,aAAa,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC,CAC9C;AAED,SAAO,OAAO,OAAO,KACnB,IAAI,WAAW;GACb,QAAQ;GACR,SAAS;GACT,MAAM,OAAO,MAAM;GACpB,CAAC,CACH;;CAGH,MAAM,aAAa,qBAAqB,UAAU,OAAO;AAEzD,KAAI,CAAC,WAAW,SAAS;EACvB,MAAM,SAAS,WAAW,MAAM,OAAO,KAAK,WAAW;GACrD,MAAM,MAAM,KAAK,KAAK,IAAI;GAC1B,SAAS,MAAM;GACf,MAAM,MAAM;GACb,EAAE;AAEH,SAAO,OAAO,WAAW,0BAA0B,CAAC,KAClD,OAAO,aAAa,EAAE,kBAAkB,QAAQ,CAAC,CAClD;EAKD,MAAM,IAAI,KAAK,UAAU;GACvB,IAAI;GACJ,MAAM;GACN,QAAQ;GACT,CAAC;AAEF,SAAO,IAAI,SAAS,GAAG;GACrB,QAAQ;GACR,SAAS,EACP,gBAAgB,oBACjB;GACF,CAAC;;CAIJ,MAAM,uBAAO,IAAI,KAAa;CAE9B,MAAM,UAAU,WAAW,KAAK,QAAQ,QAAQ;AAC9C,MAAI,KAAK,IAAI,IAAI,KAAK,CACpB,QAAO;AAGT,OAAK,IAAI,IAAI,KAAK;AAClB,SAAO;GACP;CAEF,MAAM,UAAU,WAAW,KAAK,SAAS,QAAQ;AAEjD,QAAO,OAAO,SAAS,iCAAiC,CAAC,KACvD,OAAO,aAAa;EAClB,eAAe,WAAW,KAAK;EAC/B,cAAc,QAAQ;EACtB,mBAAmB;EACpB,CAAC,CACH;CAED,MAAM,WAAW,OAAO,gBACtB,OAAO,WAAW;EAChB,WACE,MAAM,GAAG,OAAO,SAAS,UAAU,aAAa;GAC9C,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,uBAAuB,OAAO;IAC9B;IACD;GACD,MAAM,KAAK,UAAU,QAAQ;GAC9B,CAAC;EACJ,QAAQ,UACN,IAAI,WAAW;GACb,QAAQ;GACR,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;GAChE,CAAC;EACL,CAAC,EACF,kBACM,IAAI,WAAW;EAAE,QAAQ;EAAG,SAAS;EAAmB,CAAC,CAChE;CAED,MAAM,eAAe,OAAO,OAAO,WAAW;EAC5C,WAAW,SAAS,MAAM;EAC1B,aAAa;EACd,CAAC;AAEF,QAAO,IAAI,SAAS,cAAc;EAChC,QAAQ,SAAS;EACjB,SAAS,EACP,gBACE,SAAS,QAAQ,IAAI,eAAe,IAAI,oBAC3C;EACF,CAAC;EACF;AAEJ,MAAaC,OAAqB,OAAO,YAA+B;CACtE,MAAM,UAAU,OAAO,IAAI,aAAa;AACtC,SAAO,OAAO,SAAS,gCAAgC,CAAC,KACtD,OAAO,aAAa,EAAE,MAAM,QAAQ,QAAQ,UAAU,CAAC,CACxD;EAED,MAAM,SAAS,OAAO;EACtB,MAAM,gBAAgB,OAAO;AAG7B,SAAO,OAAO,aAAa,KACzB,OAAO,eACL,OAAO,SAAS,wDAAwD,CACzE,EACD,OAAO,SAAS,qBACd,OAAO,KACL,IAAI,WAAW;GACb,QAAQ;GACR,SACE;GACH,CAAC,CACH,CACF,CACF;EAGD,MAAM,eAAe,OAAO,OAAO,eAAe,KAChD,OAAO,eACL,OAAO,SAAS,4CAA4C,CAC7D,EACD,OAAO,SAAS,qBACd,OAAO,KACL,IAAI,WAAW;GACb,QAAQ;GACR,SAAS;GACV,CAAC,CACH,CACF,CACF;EAED,MAAM,SAAS,OAAO,OAAO;EAG7B,MAAM,WAAW,sBACf,kBACA,cACA,YACD,CAAC,KACA,SAAS,YAAY,MACnB,aAAa,aAAa,gBAAgB,EAAE,OAAO,GAAG,MACvD,CACF;EAED,MAAM,WAAW,OAAO,SACtB,iBACA,mBAAmB,SAAS;GAAE;GAAQ;GAAc,CAAC,EACrD,EAAE,YAAY,EAAE,EAAE,CACnB,CAAC,KACA,OAAO,MAAM,SAAS,EACtB,OAAO,UAAU,UACf,OAAO,IAAI,aAAa;AACtB,UAAO,cAAc,aAAa,OAAO,SAAS;IAChD,UAAU,QAAQ,QAAQ;IAC1B,MAAM;IACP,CAAC;AACF,UAAO,OAAO,SAAS,uBAAuB,CAAC,KAC7C,OAAO,aAAa;IAClB,OAAO,OAAO,MAAM;IACpB,QAAQ,iBAAiB,aAAa,MAAM,SAAS;IACtD,CAAC,CACH;IACD,CACH,CACF;AAED,SAAO,OAAO,SAAS,0BAA0B,CAAC,KAChD,OAAO,aAAa,EAAE,QAAQ,SAAS,QAAQ,CAAC,CACjD;AAED,SAAO;GACP,CAAC,KACD,OAAO,QAAQ,MAAM,EACrB,OAAO,SAAS,eAAe,UAC7B,OAAO,QACL,SAAS,KACP,EAAE,OAAO,MAAM,SAAS,EACxB,EAAE,QAAQ,MAAM,UAAU,kBAAkB,CAC7C,CACF,CACF,EACD,OAAO,UAAU,UAAU;AACzB,SAAO,QACL,OAAO,SAAS,oCAAoC,CAAC,KACnD,OAAO,aAAa,EAAE,OAAO,OAAO,MAAM,EAAE,CAAC,CAC9C,CACF;AACD,SAAO,OAAO,QACZ,SAAS,KAAK,EAAE,OAAO,yBAAyB,EAAE,EAAE,QAAQ,KAAK,CAAC,CACnE;GACD,CACH;AAED,QAAO,MAAM,OAAO,WAAW,oBAAoB,QAAQ,QAAQ,CAAC;;AAGtE,MAAaC,UAAwB,YACnC,OAAO,WACL,oBACE,QACA,OAAO,QACL,IAAI,SAAS,MAAM;CACjB,QAAQ;CACR,SAAS;EACP,+BAA+B;EAC/B,gCAAgC;EAChC,gCAAgC;EAChC,0BAA0B;EAC3B;CACF,CAAC,CACH,CACF,CACF;AAGH,MAAM,UAAU;AAChB,MAAM,iBAAiB;AACvB,MAAM,gBAAgB;AACtB,MAAM,QAAQ;AAEd,SAAS,sBAAsB;AAG7B,QAAO,GAAG,QAAQ,GAFF,UAAU,eAAe,CAEZ,GADd,UAAU,cAAc,CACA,GAAG;;AAG5C,SAAS,UAAU,OAAuB;CACxC,MAAM,MAAMC,IAAQ,CAAC,QAAQ,MAAM,GAAG;CACtC,MAAM,SAAS,QAAQ;AACvB,KAAI,IAAI,UAAU,OAChB,QAAO,IAAI,UAAU,GAAG,OAAO;AAEjC,SAAQ,MAAM,KAAK,UAAU,GAAG,OAAO"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NonEmptyString } from "../../lib/types.mjs";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
import * as effect_Types16 from "effect/Types";
|
|
4
|
+
import * as effect_Cause16 from "effect/Cause";
|
|
5
|
+
|
|
6
|
+
//#region src/server/services/config.service.d.ts
|
|
7
|
+
declare const ConfigError_base: new <A extends Record<string, any> = {}>(args: effect_Types16.Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }) => effect_Cause16.YieldableError & {
|
|
8
|
+
readonly _tag: "ConfigError";
|
|
9
|
+
} & Readonly<A>;
|
|
10
|
+
declare class ConfigError extends ConfigError_base<{
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}> {}
|
|
13
|
+
type ConfigService = {
|
|
14
|
+
readonly getApiUrl: Effect.Effect<string>;
|
|
15
|
+
readonly getSecretKey: Effect.Effect<NonEmptyString, ConfigError>;
|
|
16
|
+
readonly getPublicToken: Effect.Effect<string, ConfigError>;
|
|
17
|
+
};
|
|
18
|
+
declare const ConfigService: Context.Tag<ConfigService, ConfigService>;
|
|
19
|
+
declare const ConfigServiceLive: Layer.Layer<ConfigService, never, never>;
|
|
20
|
+
//#endregion
|
|
21
|
+
export { ConfigError, ConfigService, ConfigServiceLive };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.service.d.mts","names":[],"sources":["../../../src/server/services/config.service.ts"],"sourcesContent":[],"mappings":";;;;;;cAIyD;;;cAI5C,WAAA,SAAoB;EAJwB,SAAA,OAAA,EAAA,MAAA;;KAQ7C,aAAA;sBACU,MAAA,CAAO;yBACJ,MAAA,CAAO,OAAO,gBAAgB;2BAC5B,MAAA,CAAO,eAAe;;cAGpC,eAAa,OAAA,CAAA,IAAA,eAAA;cAsDb,mBAAiB,KAAA,CAAA,MAAA"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { normalizeSecretKey, resolveApiUrl } from "../../build/env-config.mjs";
|
|
2
|
+
import { Context, Data, Effect, Layer } from "effect";
|
|
3
|
+
import { getPublicToken } from "@interfere/react/server/auth";
|
|
4
|
+
|
|
5
|
+
//#region src/server/services/config.service.ts
|
|
6
|
+
const ONE_HOUR_MS = 36e5;
|
|
7
|
+
var ConfigError = class extends Data.TaggedError("ConfigError") {};
|
|
8
|
+
const ConfigService = Context.GenericTag("@interfere/next/ConfigService");
|
|
9
|
+
let cachedPublicToken = null;
|
|
10
|
+
const getSecretKey = () => Effect.gen(function* () {
|
|
11
|
+
const normalized = normalizeSecretKey(process.env.INTERFERE_SECRET_KEY);
|
|
12
|
+
if (!normalized) return yield* Effect.fail(new ConfigError({ message: "INTERFERE_SECRET_KEY not set or empty" }));
|
|
13
|
+
return normalized;
|
|
14
|
+
});
|
|
15
|
+
const getPublicTokenImpl = () => Effect.gen(function* () {
|
|
16
|
+
const override = process.env.INTERFERE_PUBLIC_TOKEN;
|
|
17
|
+
if (override) return override;
|
|
18
|
+
const secret = yield* getSecretKey();
|
|
19
|
+
const apiUrl = resolveApiUrl();
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
if (cachedPublicToken && cachedPublicToken.expiresAt > now) return cachedPublicToken.token;
|
|
22
|
+
const token = yield* Effect.tryPromise({
|
|
23
|
+
try: () => getPublicToken({
|
|
24
|
+
apiUrl,
|
|
25
|
+
secret
|
|
26
|
+
}),
|
|
27
|
+
catch: (error) => new ConfigError({ message: `Failed to get public token: ${error}` })
|
|
28
|
+
});
|
|
29
|
+
if (!token) return yield* Effect.fail(new ConfigError({ message: "No token returned" }));
|
|
30
|
+
cachedPublicToken = {
|
|
31
|
+
token,
|
|
32
|
+
expiresAt: now + ONE_HOUR_MS
|
|
33
|
+
};
|
|
34
|
+
return token;
|
|
35
|
+
});
|
|
36
|
+
const ConfigServiceLive = Layer.succeed(ConfigService, ConfigService.of({
|
|
37
|
+
getApiUrl: Effect.succeed(resolveApiUrl()),
|
|
38
|
+
getSecretKey: getSecretKey(),
|
|
39
|
+
getPublicToken: getPublicTokenImpl()
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
//#endregion
|
|
43
|
+
export { ConfigError, ConfigService, ConfigServiceLive };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.service.mjs","names":["cachedPublicToken: { token: string; expiresAt: number } | null","getPublicTokenShared"],"sources":["../../../src/server/services/config.service.ts"],"sourcesContent":["import { getPublicToken as getPublicTokenShared } from \"@interfere/react/server/auth\";\nimport { Context, Data, Effect, Layer } from \"effect\";\n\nimport { normalizeSecretKey, resolveApiUrl } from \"../../build/env-config.js\";\nimport type { NonEmptyString } from \"../../lib/types.js\";\n\nconst ONE_HOUR_MS = 3_600_000;\n\nexport class ConfigError extends Data.TaggedError(\"ConfigError\")<{\n readonly message: string;\n}> {}\n\nexport type ConfigService = {\n readonly getApiUrl: Effect.Effect<string>;\n readonly getSecretKey: Effect.Effect<NonEmptyString, ConfigError>;\n readonly getPublicToken: Effect.Effect<string, ConfigError>;\n};\n\nexport const ConfigService = Context.GenericTag<ConfigService>(\n \"@interfere/next/ConfigService\"\n);\n\nlet cachedPublicToken: { token: string; expiresAt: number } | null = null;\n\nconst getSecretKey = (): Effect.Effect<NonEmptyString, ConfigError> =>\n Effect.gen(function* () {\n const normalized = normalizeSecretKey(process.env.INTERFERE_SECRET_KEY);\n\n if (!normalized) {\n return yield* Effect.fail(\n new ConfigError({ message: \"INTERFERE_SECRET_KEY not set or empty\" })\n );\n }\n\n return normalized;\n });\n\nconst getPublicTokenImpl = (): Effect.Effect<string, ConfigError> =>\n Effect.gen(function* () {\n const override = process.env.INTERFERE_PUBLIC_TOKEN;\n if (override) {\n return override;\n }\n\n const secret = yield* getSecretKey();\n const apiUrl = resolveApiUrl();\n\n const now = Date.now();\n if (cachedPublicToken && cachedPublicToken.expiresAt > now) {\n return cachedPublicToken.token;\n }\n\n const token = yield* Effect.tryPromise({\n try: () => getPublicTokenShared({ apiUrl, secret }),\n catch: (error) =>\n new ConfigError({ message: `Failed to get public token: ${error}` }),\n });\n\n if (!token) {\n return yield* Effect.fail(\n new ConfigError({ message: \"No token returned\" })\n );\n }\n\n cachedPublicToken = {\n token,\n expiresAt: now + ONE_HOUR_MS,\n };\n\n return token;\n });\n\nexport const ConfigServiceLive = Layer.succeed(\n ConfigService,\n ConfigService.of({\n getApiUrl: Effect.succeed(resolveApiUrl()),\n getSecretKey: getSecretKey(),\n getPublicToken: getPublicTokenImpl(),\n })\n);\n"],"mappings":";;;;;AAMA,MAAM,cAAc;AAEpB,IAAa,cAAb,cAAiC,KAAK,YAAY,cAAc,CAE7D;AAQH,MAAa,gBAAgB,QAAQ,WACnC,gCACD;AAED,IAAIA,oBAAiE;AAErE,MAAM,qBACJ,OAAO,IAAI,aAAa;CACtB,MAAM,aAAa,mBAAmB,QAAQ,IAAI,qBAAqB;AAEvE,KAAI,CAAC,WACH,QAAO,OAAO,OAAO,KACnB,IAAI,YAAY,EAAE,SAAS,yCAAyC,CAAC,CACtE;AAGH,QAAO;EACP;AAEJ,MAAM,2BACJ,OAAO,IAAI,aAAa;CACtB,MAAM,WAAW,QAAQ,IAAI;AAC7B,KAAI,SACF,QAAO;CAGT,MAAM,SAAS,OAAO,cAAc;CACpC,MAAM,SAAS,eAAe;CAE9B,MAAM,MAAM,KAAK,KAAK;AACtB,KAAI,qBAAqB,kBAAkB,YAAY,IACrD,QAAO,kBAAkB;CAG3B,MAAM,QAAQ,OAAO,OAAO,WAAW;EACrC,WAAWC,eAAqB;GAAE;GAAQ;GAAQ,CAAC;EACnD,QAAQ,UACN,IAAI,YAAY,EAAE,SAAS,+BAA+B,SAAS,CAAC;EACvE,CAAC;AAEF,KAAI,CAAC,MACH,QAAO,OAAO,OAAO,KACnB,IAAI,YAAY,EAAE,SAAS,qBAAqB,CAAC,CAClD;AAGH,qBAAoB;EAClB;EACA,WAAW,MAAM;EAClB;AAED,QAAO;EACP;AAEJ,MAAa,oBAAoB,MAAM,QACrC,eACA,cAAc,GAAG;CACf,WAAW,OAAO,QAAQ,eAAe,CAAC;CAC1C,cAAc,cAAc;CAC5B,gBAAgB,oBAAoB;CACrC,CAAC,CACH"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ConfigService } from "./config.service.mjs";
|
|
2
|
+
import { Context, Effect, Layer } from "effect";
|
|
3
|
+
import * as effect_Types15 from "effect/Types";
|
|
4
|
+
import * as effect_Cause15 from "effect/Cause";
|
|
5
|
+
|
|
6
|
+
//#region src/server/services/error-tracking.service.d.ts
|
|
7
|
+
declare const ErrorTrackingError_base: new <A extends Record<string, any> = {}>(args: effect_Types15.Equals<A, {}> extends true ? void : { readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }) => effect_Cause15.YieldableError & {
|
|
8
|
+
readonly _tag: "ErrorTrackingError";
|
|
9
|
+
} & Readonly<A>;
|
|
10
|
+
declare class ErrorTrackingError extends ErrorTrackingError_base<{
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}> {}
|
|
13
|
+
type ErrorTrackingService = {
|
|
14
|
+
readonly captureError: (error: unknown, request?: Request, context?: Record<string, unknown>) => Effect.Effect<void, never, ConfigService>;
|
|
15
|
+
};
|
|
16
|
+
declare const ErrorTrackingService: Context.Tag<ErrorTrackingService, ErrorTrackingService>;
|
|
17
|
+
declare const ErrorTrackingServiceLive: Layer.Layer<ErrorTrackingService, never, never>;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { ErrorTrackingError, ErrorTrackingService, ErrorTrackingServiceLive };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-tracking.service.d.mts","names":[],"sources":["../../../src/server/services/error-tracking.service.ts"],"sourcesContent":[],"mappings":";;;;;;cAOiD;;;cAEpC,kBAAA,SAA2B;EAFS,SAAA,OAAA,EAAA,MAAA;;KAMrC,oBAAA;oDAGE,mBACA,4BACP,MAAA,CAAO,oBAAoB;;cAGrB,sBAAoB,OAAA,CAAA,IAAA,sBAAA;cAIpB,0BAAwB,KAAA,CAAA,MAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ConfigService } from "./config.service.mjs";
|
|
2
|
+
import { API_PATHS } from "@interfere/constants/api";
|
|
3
|
+
import { Context, Data, Effect, Layer } from "effect";
|
|
4
|
+
import { getRuntime } from "@interfere/react/core/runtime/config";
|
|
5
|
+
import { buildServerErrorEnvelope, sendEnvelopesToIngest } from "@interfere/react/server/capture";
|
|
6
|
+
|
|
7
|
+
//#region src/server/services/error-tracking.service.ts
|
|
8
|
+
var ErrorTrackingError = class extends Data.TaggedError("ErrorTrackingError") {};
|
|
9
|
+
const ErrorTrackingService = Context.GenericTag("@interfere/next/ErrorTrackingService");
|
|
10
|
+
const ErrorTrackingServiceLive = Layer.succeed(ErrorTrackingService, ErrorTrackingService.of({ captureError: (error, request, context) => Effect.gen(function* () {
|
|
11
|
+
const config = yield* ConfigService;
|
|
12
|
+
const token = yield* config.getPublicToken.pipe(Effect.catchAll(() => Effect.succeed(null)));
|
|
13
|
+
if (!token) return;
|
|
14
|
+
const apiUrl = yield* config.getApiUrl;
|
|
15
|
+
const envelope = buildServerErrorEnvelope({
|
|
16
|
+
buildId: process.env.NEXT_PUBLIC_INTERFERE_BUILD_ID ?? null,
|
|
17
|
+
releaseId: process.env.NEXT_PUBLIC_INTERFERE_RELEASE_ID ?? null,
|
|
18
|
+
environment: process.env.NODE_ENV || null,
|
|
19
|
+
runtime: getRuntime(),
|
|
20
|
+
error,
|
|
21
|
+
request,
|
|
22
|
+
context: { ...context }
|
|
23
|
+
});
|
|
24
|
+
yield* Effect.tryPromise({
|
|
25
|
+
try: () => sendEnvelopesToIngest([envelope], `${apiUrl}${API_PATHS.INGEST_V0}`, token),
|
|
26
|
+
catch: () => new ErrorTrackingError({ message: "Failed to send error to ingest" })
|
|
27
|
+
}).pipe(Effect.catchAll(() => Effect.void));
|
|
28
|
+
}) }));
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ErrorTrackingError, ErrorTrackingService, ErrorTrackingServiceLive };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-tracking.service.mjs","names":[],"sources":["../../../src/server/services/error-tracking.service.ts"],"sourcesContent":["import { API_PATHS } from \"@interfere/constants/api\";\nimport { getRuntime } from \"@interfere/react/core/runtime/config\";\nimport {\n buildServerErrorEnvelope,\n sendEnvelopesToIngest,\n} from \"@interfere/react/server/capture\";\nimport { Context, Data, Effect, Layer } from \"effect\";\nimport { ConfigService } from \"./config.service\";\n\nexport class ErrorTrackingError extends Data.TaggedError(\"ErrorTrackingError\")<{\n readonly message: string;\n}> {}\n\nexport type ErrorTrackingService = {\n readonly captureError: (\n error: unknown,\n request?: Request,\n context?: Record<string, unknown>\n ) => Effect.Effect<void, never, ConfigService>;\n};\n\nexport const ErrorTrackingService = Context.GenericTag<ErrorTrackingService>(\n \"@interfere/next/ErrorTrackingService\"\n);\n\nexport const ErrorTrackingServiceLive = Layer.succeed(\n ErrorTrackingService,\n ErrorTrackingService.of({\n captureError: (error, request, context) =>\n Effect.gen(function* () {\n const config = yield* ConfigService;\n const token = yield* config.getPublicToken.pipe(\n Effect.catchAll(() => Effect.succeed(null))\n );\n\n if (!token) {\n return;\n }\n\n const apiUrl = yield* config.getApiUrl;\n const inferredValues = {\n buildId: process.env.NEXT_PUBLIC_INTERFERE_BUILD_ID ?? null,\n releaseId: process.env.NEXT_PUBLIC_INTERFERE_RELEASE_ID ?? null,\n environment: process.env.NODE_ENV || null,\n runtime: getRuntime(),\n };\n\n const envelope = buildServerErrorEnvelope({\n ...inferredValues,\n error,\n request,\n context: {\n ...context,\n },\n });\n\n yield* Effect.tryPromise({\n try: () =>\n sendEnvelopesToIngest(\n [envelope],\n `${apiUrl}${API_PATHS.INGEST_V0}`,\n token\n ),\n catch: () =>\n new ErrorTrackingError({\n message: \"Failed to send error to ingest\",\n }),\n }).pipe(\n Effect.catchAll(() => Effect.void) // Best-effort\n );\n }),\n })\n);\n"],"mappings":";;;;;;;AASA,IAAa,qBAAb,cAAwC,KAAK,YAAY,qBAAqB,CAE3E;AAUH,MAAa,uBAAuB,QAAQ,WAC1C,uCACD;AAED,MAAa,2BAA2B,MAAM,QAC5C,sBACA,qBAAqB,GAAG,EACtB,eAAe,OAAO,SAAS,YAC7B,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO;CACtB,MAAM,QAAQ,OAAO,OAAO,eAAe,KACzC,OAAO,eAAe,OAAO,QAAQ,KAAK,CAAC,CAC5C;AAED,KAAI,CAAC,MACH;CAGF,MAAM,SAAS,OAAO,OAAO;CAQ7B,MAAM,WAAW,yBAAyB;EANxC,SAAS,QAAQ,IAAI,kCAAkC;EACvD,WAAW,QAAQ,IAAI,oCAAoC;EAC3D,aAAa,QAAQ,IAAI,YAAY;EACrC,SAAS,YAAY;EAKrB;EACA;EACA,SAAS,EACP,GAAG,SACJ;EACF,CAAC;AAEF,QAAO,OAAO,WAAW;EACvB,WACE,sBACE,CAAC,SAAS,EACV,GAAG,SAAS,UAAU,aACtB,MACD;EACH,aACE,IAAI,mBAAmB,EACrB,SAAS,kCACV,CAAC;EACL,CAAC,CAAC,KACD,OAAO,eAAe,OAAO,KAAK,CACnC;EACD,EACL,CAAC,CACH"}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interfere/next",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.15-alpha.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Build apps that never break.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"observability",
|
|
8
8
|
"typescript",
|
|
9
|
-
"
|
|
10
|
-
"logging"
|
|
9
|
+
"react",
|
|
10
|
+
"logging",
|
|
11
|
+
"error-tracking",
|
|
12
|
+
"session-replay"
|
|
11
13
|
],
|
|
12
14
|
"homepage": "https://interfere.com",
|
|
13
15
|
"bugs": {
|
|
@@ -18,56 +20,85 @@
|
|
|
18
20
|
"dist"
|
|
19
21
|
],
|
|
20
22
|
"type": "module",
|
|
21
|
-
"main": "./dist/index.jsx",
|
|
22
|
-
"types": "./dist/index.d.ts",
|
|
23
23
|
"exports": {
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
24
|
+
"./build": {
|
|
25
|
+
"@source": "./src/build/with-interfere.ts",
|
|
26
|
+
"types": "./dist/build/with-interfere.d.mts",
|
|
27
|
+
"default": "./dist/build/with-interfere.mjs"
|
|
28
28
|
},
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
29
|
+
"./provider": {
|
|
30
|
+
"@source": "./src/client/provider.tsx",
|
|
31
|
+
"types": "./dist/client/provider.d.mts",
|
|
32
|
+
"default": "./dist/client/provider.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./client": {
|
|
35
|
+
"@source": "./src/client/client.ts",
|
|
36
|
+
"types": "./dist/client/client.d.mts",
|
|
37
|
+
"default": "./dist/client/client.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./route-handler": {
|
|
40
|
+
"@source": "./src/server/route-handler.ts",
|
|
41
|
+
"types": "./dist/server/route-handler.d.mts",
|
|
42
|
+
"default": "./dist/server/route-handler.mjs"
|
|
43
|
+
},
|
|
44
|
+
"./server/route-handler": {
|
|
45
|
+
"@source": "./src/server/route-handler.ts",
|
|
46
|
+
"types": "./dist/server/route-handler.d.mts",
|
|
47
|
+
"default": "./dist/server/route-handler.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./middleware": {
|
|
50
|
+
"@source": "./src/server/middleware.ts",
|
|
51
|
+
"types": "./dist/server/middleware.d.mts",
|
|
52
|
+
"default": "./dist/server/middleware.mjs"
|
|
53
|
+
},
|
|
54
|
+
"./proxy": {
|
|
55
|
+
"@source": "./src/server/proxy.ts",
|
|
56
|
+
"types": "./dist/server/proxy.d.mts",
|
|
57
|
+
"default": "./dist/server/proxy.mjs"
|
|
33
58
|
}
|
|
34
59
|
},
|
|
35
60
|
"publishConfig": {
|
|
36
61
|
"access": "public"
|
|
37
62
|
},
|
|
38
63
|
"dependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"glob": "^
|
|
43
|
-
"nanoid": "^5.1.6",
|
|
44
|
-
"node-fetch": "^3.3.2",
|
|
45
|
-
"rrweb": "2.0.0-alpha.18",
|
|
64
|
+
"@effect/platform": "^0.94.0",
|
|
65
|
+
"chalk": "^5.6.2",
|
|
66
|
+
"effect": "^3.19.13",
|
|
67
|
+
"glob": "^13.0.0",
|
|
46
68
|
"uuid": "^13.0.0",
|
|
47
|
-
"
|
|
69
|
+
"zod": "^4.2.1",
|
|
70
|
+
"@interfere/constants": "0.0.2-alpha.0",
|
|
71
|
+
"@interfere/effect-utils": "0.0.2-alpha.0",
|
|
72
|
+
"@interfere/react": "0.0.2-alpha.0",
|
|
73
|
+
"@interfere/types": "0.0.2-alpha.0"
|
|
48
74
|
},
|
|
49
75
|
"peerDependencies": {
|
|
50
|
-
"
|
|
76
|
+
"@vercel/blob": "^2",
|
|
77
|
+
"next": ">=15 || >=16",
|
|
51
78
|
"react": ">=19",
|
|
52
79
|
"react-dom": ">=19"
|
|
53
80
|
},
|
|
54
81
|
"devDependencies": {
|
|
55
|
-
"@types/node": "22.
|
|
56
|
-
"@types/react": "19.
|
|
57
|
-
"@types/react-dom": "19.
|
|
58
|
-
"@vitest/coverage-v8": "^
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
82
|
+
"@types/node": "^22.19.3",
|
|
83
|
+
"@types/react": "19.2.7",
|
|
84
|
+
"@types/react-dom": "19.2.3",
|
|
85
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
86
|
+
"jsdom": "^27.3.0",
|
|
87
|
+
"msw": "^2.12.4",
|
|
88
|
+
"next": "^16.1.0",
|
|
89
|
+
"react": "^19.2.3",
|
|
90
|
+
"react-dom": "^19.2.3",
|
|
91
|
+
"typescript": "5.9.3",
|
|
92
|
+
"vitest": "^4.0.16",
|
|
93
|
+
"webpack": "^5.104.1",
|
|
94
|
+
"@interfere/test-utils": "0.0.0",
|
|
95
|
+
"@interfere/typescript-config": "1.0.3-alpha.0",
|
|
96
|
+
"@interfere/vitest-config": "1.0.1-alpha.0"
|
|
66
97
|
},
|
|
67
98
|
"scripts": {
|
|
68
|
-
"build": "
|
|
69
|
-
"dev": "
|
|
70
|
-
"
|
|
99
|
+
"build": "tsdown",
|
|
100
|
+
"dev": "tsdown --watch",
|
|
101
|
+
"typecheck": "tsc --noEmit --incremental",
|
|
71
102
|
"test": "vitest run --coverage"
|
|
72
103
|
}
|
|
73
104
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"with-interfere-coverage.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/build/with-interfere-coverage.test.ts"],"names":[],"mappings":""}
|