@browserlessapi/inferlo 0.4.0 → 0.6.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 +21 -3
- package/dist/adonisjs.d.ts +33 -0
- package/dist/adonisjs.js +30 -0
- package/dist/adonisjs.js.map +1 -0
- package/dist/ai/index.d.ts +22 -8
- package/dist/ai/index.js +107 -15
- package/dist/ai/index.js.map +1 -1
- package/dist/ai/providers.js +5 -4
- package/dist/ai/providers.js.map +1 -1
- package/dist/ai/public.d.ts +21 -0
- package/dist/ai/public.js +17 -0
- package/dist/ai/public.js.map +1 -0
- package/dist/ai/tool.d.ts +47 -0
- package/dist/ai/tool.js +85 -0
- package/dist/ai/tool.js.map +1 -0
- package/dist/ai-client.d.ts +3 -0
- package/dist/ai-client.js +8 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/api-version.d.ts +32 -0
- package/dist/api-version.js +45 -0
- package/dist/api-version.js.map +1 -0
- package/dist/app.d.ts +117 -5
- package/dist/app.js +667 -156
- package/dist/app.js.map +1 -1
- package/dist/audit.d.ts +50 -0
- package/dist/audit.js +92 -0
- package/dist/audit.js.map +1 -0
- package/dist/cache.d.ts +92 -0
- package/dist/cache.js +245 -0
- package/dist/cache.js.map +1 -0
- package/dist/cli/main.js +45 -5
- package/dist/cli/main.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/db/table.d.ts +31 -0
- package/dist/db/table.js +140 -1
- package/dist/db/table.js.map +1 -1
- package/dist/deprecation.d.ts +23 -0
- package/dist/deprecation.js +70 -0
- package/dist/deprecation.js.map +1 -0
- package/dist/describe.d.ts +68 -5
- package/dist/describe.js +38 -3
- package/dist/describe.js.map +1 -1
- package/dist/elysia.d.ts +49 -0
- package/dist/elysia.js +10 -0
- package/dist/elysia.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.js +5 -4
- package/dist/env.js.map +1 -1
- package/dist/express.d.ts +38 -0
- package/dist/express.js +18 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.d.ts +52 -0
- package/dist/fastify.js +65 -0
- package/dist/fastify.js.map +1 -0
- package/dist/h3.d.ts +76 -0
- package/dist/h3.js +61 -0
- package/dist/h3.js.map +1 -0
- package/dist/hapi.d.ts +45 -0
- package/dist/hapi.js +57 -0
- package/dist/hapi.js.map +1 -0
- package/dist/hono.d.ts +26 -0
- package/dist/hono.js +10 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.d.ts +27 -5
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/jobs.d.ts +24 -0
- package/dist/jobs.js +35 -6
- package/dist/jobs.js.map +1 -1
- package/dist/koa.d.ts +51 -0
- package/dist/koa.js +45 -0
- package/dist/koa.js.map +1 -0
- package/dist/lambda.d.ts +110 -0
- package/dist/lambda.js +169 -0
- package/dist/lambda.js.map +1 -0
- package/dist/logging.d.ts +23 -0
- package/dist/logging.js +87 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp.d.ts +1 -7
- package/dist/mcp.js +18 -4
- package/dist/mcp.js.map +1 -1
- package/dist/metrics.d.ts +48 -0
- package/dist/metrics.js +184 -0
- package/dist/metrics.js.map +1 -0
- package/dist/module.d.ts +52 -0
- package/dist/module.js +152 -0
- package/dist/module.js.map +1 -0
- package/dist/nestjs.d.ts +43 -0
- package/dist/nestjs.js +47 -0
- package/dist/nestjs.js.map +1 -0
- package/dist/nextjs.d.ts +35 -0
- package/dist/nextjs.js +33 -0
- package/dist/nextjs.js.map +1 -0
- package/dist/openapi.d.ts +3 -1
- package/dist/openapi.js +16 -3
- package/dist/openapi.js.map +1 -1
- package/dist/permissions.d.ts +26 -0
- package/dist/permissions.js +78 -0
- package/dist/permissions.js.map +1 -0
- package/dist/react-router.d.ts +41 -0
- package/dist/react-router.js +42 -0
- package/dist/react-router.js.map +1 -0
- package/dist/redact.d.ts +19 -0
- package/dist/redact.js +150 -0
- package/dist/redact.js.map +1 -0
- package/dist/request-matcher.d.ts +4 -0
- package/dist/request-matcher.js +14 -0
- package/dist/request-matcher.js.map +1 -0
- package/dist/route.d.ts +54 -1
- package/dist/route.js +24 -0
- package/dist/route.js.map +1 -1
- package/dist/secure-headers.d.ts +8 -0
- package/dist/secure-headers.js +61 -0
- package/dist/secure-headers.js.map +1 -0
- package/dist/serve.d.ts +72 -1
- package/dist/serve.js +201 -61
- package/dist/serve.js.map +1 -1
- package/dist/sessions.d.ts +72 -0
- package/dist/sessions.js +171 -0
- package/dist/sessions.js.map +1 -0
- package/dist/sveltekit.d.ts +72 -0
- package/dist/sveltekit.js +68 -0
- package/dist/sveltekit.js.map +1 -0
- package/dist/tracing.d.ts +118 -0
- package/dist/tracing.js +204 -0
- package/dist/tracing.js.map +1 -0
- package/dist/trpc.d.ts +57 -0
- package/dist/trpc.js +142 -0
- package/dist/trpc.js.map +1 -0
- package/dist/tsed.d.ts +32 -0
- package/dist/tsed.js +13 -0
- package/dist/tsed.js.map +1 -0
- package/llms.txt +15 -5
- package/package.json +99 -3
- package/templates/app/AGENTS.md +12 -6
- package/templates/app/_env.example +2 -0
- package/templates/app/src/app.ts +15 -8
- package/templates/app/src/config.ts +17 -0
- package/templates/app/src/server.ts +9 -9
- package/templates/app/src/routes/health.ts +0 -3
package/dist/sessions.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { safeEqual } from "./auth.js";
|
|
2
|
+
import { InferloError } from "./errors.js";
|
|
3
|
+
const DAY = 86_400_000;
|
|
4
|
+
const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
|
|
5
|
+
const MAX_FORM_BYTES = 64 * 1024;
|
|
6
|
+
const fix = 'Pass sessions({ secret: config.SESSION_SECRET, user: ({ userId }, { db }) => db.users.find(userId) }) with a secret of at least 32 random characters (openssl rand -base64 32).';
|
|
7
|
+
/** Signed cookie sessions with CSRF protection: createApp({ auth: sessions({ secret, user }) }). */
|
|
8
|
+
export function sessions(options) {
|
|
9
|
+
if (!options || typeof options !== "object")
|
|
10
|
+
throw new InferloError("IL6029", "sessions() needs { secret, user }.", { fix });
|
|
11
|
+
const secrets = typeof options.secret === "string" ? [options.secret] : Array.isArray(options.secret) ? [...options.secret] : [];
|
|
12
|
+
if (!secrets.length || secrets.some((s) => typeof s !== "string" || s.length < 32)) {
|
|
13
|
+
throw new InferloError("IL6029", "sessions() secret must be a string of at least 32 characters (or a list of them, newest first).", { fix });
|
|
14
|
+
}
|
|
15
|
+
if (typeof options.user !== "function")
|
|
16
|
+
throw new InferloError("IL6029", "sessions() needs a user({ userId }, { db }) function that loads the signed-in user.", { fix });
|
|
17
|
+
const maxAgeMs = options.maxAgeMs ?? 7 * DAY;
|
|
18
|
+
if (!Number.isInteger(maxAgeMs) || maxAgeMs < 60_000 || maxAgeMs > 400 * DAY) {
|
|
19
|
+
throw new InferloError("IL6029", "sessions() maxAgeMs must be a whole number of milliseconds from 60000 (1 minute) to 34560000000 (400 days).", { fix });
|
|
20
|
+
}
|
|
21
|
+
const cookie = options.cookie ?? {};
|
|
22
|
+
const name = cookie.name ?? "inferlo_session";
|
|
23
|
+
if (!/^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/.test(name))
|
|
24
|
+
throw new InferloError("IL6029", `sessions() cookie name "${name}" is not a valid cookie name.`, { fix: 'Use letters, digits, "-" and "_", such as "app_session".' });
|
|
25
|
+
if (cookie.sameSite !== undefined && cookie.sameSite !== "lax" && cookie.sameSite !== "strict") {
|
|
26
|
+
throw new InferloError("IL6029", 'sessions() cookie.sameSite must be "lax" or "strict".', { fix: 'Use sameSite: "lax" (the default) or "strict".' });
|
|
27
|
+
}
|
|
28
|
+
for (const key of ["domain", "path"]) {
|
|
29
|
+
const value = cookie[key];
|
|
30
|
+
if (value !== undefined && (typeof value !== "string" || !value || /[;,\s]/.test(value))) {
|
|
31
|
+
throw new InferloError("IL6029", `sessions() cookie.${key} must be a non-empty string without spaces, commas or semicolons.`, { fix });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const csrfName = `${name}_csrf`;
|
|
35
|
+
const sameSite = cookie.sameSite === "strict" ? "Strict" : "Lax";
|
|
36
|
+
const path = cookie.path ?? "/";
|
|
37
|
+
const keys = secrets.map((secret) => crypto.subtle.importKey("raw", new TextEncoder().encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]));
|
|
38
|
+
const sign = async (key, text) => base64url(new Uint8Array(await crypto.subtle.sign("HMAC", await key, new TextEncoder().encode(text))));
|
|
39
|
+
const csrfFor = (sessionId) => sign(keys[0], `csrf.${sessionId}`);
|
|
40
|
+
const attributes = (url, httpOnly, maxAge) => {
|
|
41
|
+
const local = url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
42
|
+
const secure = cookie.secure ?? !local;
|
|
43
|
+
return [`Path=${path}`, cookie.domain ? `Domain=${cookie.domain}` : "", `Max-Age=${maxAge}`, httpOnly ? "HttpOnly" : "", secure ? "Secure" : "", `SameSite=${sameSite}`].filter(Boolean).join("; ");
|
|
44
|
+
};
|
|
45
|
+
/** The verified session of a request, or null. */
|
|
46
|
+
const read = async (req) => {
|
|
47
|
+
const value = readCookie(req.headers.get("cookie"), name);
|
|
48
|
+
if (!value)
|
|
49
|
+
return null;
|
|
50
|
+
const [version, payload, signature, extra] = value.split(".");
|
|
51
|
+
if (version !== "v1" || !payload || !signature || extra !== undefined)
|
|
52
|
+
return null;
|
|
53
|
+
let valid = false;
|
|
54
|
+
for (const key of keys)
|
|
55
|
+
if (safeEqual(await sign(key, `v1.${payload}`), signature))
|
|
56
|
+
valid = true;
|
|
57
|
+
if (!valid)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(new TextDecoder().decode(unbase64url(payload)));
|
|
61
|
+
if (typeof parsed.u !== "string" || typeof parsed.s !== "string" || typeof parsed.i !== "number" || typeof parsed.e !== "number")
|
|
62
|
+
return null;
|
|
63
|
+
if (parsed.e <= Date.now())
|
|
64
|
+
return null;
|
|
65
|
+
return { data: { userId: parsed.u, issuedAt: parsed.i }, sessionId: parsed.s };
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const session = {
|
|
72
|
+
async resolve(req, { db }) {
|
|
73
|
+
const current = await read(req);
|
|
74
|
+
if (!current)
|
|
75
|
+
return null;
|
|
76
|
+
if (!SAFE_METHODS.has(req.method.toUpperCase())) {
|
|
77
|
+
const sent = req.headers.get("x-csrf-token") ?? (await formField(req, "_csrf"));
|
|
78
|
+
if (!sent || !safeEqual(sent, await csrfFor(current.sessionId))) {
|
|
79
|
+
throw new InferloError("IL3005", "Missing or invalid CSRF token.", {
|
|
80
|
+
status: 403,
|
|
81
|
+
fix: "Send the session's CSRF token in an X-CSRF-Token header (from session.signIn or the " + csrfName + " cookie) or a _csrf form field (from session.csrfToken(req)).",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return (await options.user(current.data, { db, req })) ?? null;
|
|
86
|
+
},
|
|
87
|
+
roles: options.roles,
|
|
88
|
+
async signIn(ctx, userId) {
|
|
89
|
+
if (typeof userId !== "string" || !userId)
|
|
90
|
+
throw new InferloError("IL6029", "session.signIn(ctx, userId) needs the user's id as a non-empty string.", { fix: "Call it after checking the password: await session.signIn(ctx, user.id)." });
|
|
91
|
+
if (!ctx || typeof ctx.header !== "function" || !(ctx.url instanceof URL))
|
|
92
|
+
throw new InferloError("IL6029", "session.signIn(ctx, userId) needs the route context.", { fix: "Pass the handler's context: route.post(\"/login\", ...).handle(async (ctx) => { await session.signIn(ctx, user.id) })." });
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const sessionId = base64url(crypto.getRandomValues(new Uint8Array(18)));
|
|
95
|
+
const payload = base64url(new TextEncoder().encode(JSON.stringify({ u: userId, s: sessionId, i: now, e: now + maxAgeMs })));
|
|
96
|
+
const value = `v1.${payload}.${await sign(keys[0], `v1.${payload}`)}`;
|
|
97
|
+
const csrfToken = await csrfFor(sessionId);
|
|
98
|
+
const seconds = Math.floor(maxAgeMs / 1000);
|
|
99
|
+
ctx.header("set-cookie", `${name}=${value}; ${attributes(ctx.url, true, seconds)}`);
|
|
100
|
+
ctx.header("set-cookie", `${csrfName}=${csrfToken}; ${attributes(ctx.url, false, seconds)}`);
|
|
101
|
+
return { csrfToken };
|
|
102
|
+
},
|
|
103
|
+
signOut(ctx) {
|
|
104
|
+
if (!ctx || typeof ctx.header !== "function" || !(ctx.url instanceof URL))
|
|
105
|
+
throw new InferloError("IL6029", "session.signOut(ctx) needs the route context.", { fix: "Pass the handler's context: session.signOut(ctx)." });
|
|
106
|
+
ctx.header("set-cookie", `${name}=; ${attributes(ctx.url, true, 0)}`);
|
|
107
|
+
ctx.header("set-cookie", `${csrfName}=; ${attributes(ctx.url, false, 0)}`);
|
|
108
|
+
},
|
|
109
|
+
async csrfToken(req) {
|
|
110
|
+
const current = await read(req);
|
|
111
|
+
return current ? csrfFor(current.sessionId) : null;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
return Object.freeze(session);
|
|
115
|
+
}
|
|
116
|
+
function readCookie(header, name) {
|
|
117
|
+
if (!header)
|
|
118
|
+
return undefined;
|
|
119
|
+
for (const part of header.split(";")) {
|
|
120
|
+
const i = part.indexOf("=");
|
|
121
|
+
if (i > 0 && part.slice(0, i).trim() === name)
|
|
122
|
+
return part.slice(i + 1).trim();
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
/** A field of a URL-encoded form body, read from a copy of the request (at most 64 KiB) so the route can still read it. */
|
|
127
|
+
async function formField(req, field) {
|
|
128
|
+
if (!(req.headers.get("content-type") ?? "").toLowerCase().includes("application/x-www-form-urlencoded"))
|
|
129
|
+
return null;
|
|
130
|
+
if (Number(req.headers.get("content-length") ?? "0") > MAX_FORM_BYTES)
|
|
131
|
+
return null;
|
|
132
|
+
// Cloned before req.body is touched: on Bun 1.3, a body stream read before clone() comes back empty.
|
|
133
|
+
const body = req.clone().body;
|
|
134
|
+
if (!body)
|
|
135
|
+
return null;
|
|
136
|
+
const reader = body.getReader();
|
|
137
|
+
const chunks = [];
|
|
138
|
+
let size = 0;
|
|
139
|
+
try {
|
|
140
|
+
while (size <= MAX_FORM_BYTES) {
|
|
141
|
+
const { done, value } = await reader.read();
|
|
142
|
+
if (done)
|
|
143
|
+
break;
|
|
144
|
+
chunks.push(value);
|
|
145
|
+
size += value.byteLength;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
void reader.cancel().catch(() => { });
|
|
150
|
+
}
|
|
151
|
+
if (size > MAX_FORM_BYTES)
|
|
152
|
+
return null;
|
|
153
|
+
const bytes = new Uint8Array(size);
|
|
154
|
+
let offset = 0;
|
|
155
|
+
for (const chunk of chunks) {
|
|
156
|
+
bytes.set(chunk, offset);
|
|
157
|
+
offset += chunk.byteLength;
|
|
158
|
+
}
|
|
159
|
+
return new URLSearchParams(new TextDecoder().decode(bytes)).get(field);
|
|
160
|
+
}
|
|
161
|
+
function base64url(bytes) {
|
|
162
|
+
let text = "";
|
|
163
|
+
for (const b of bytes)
|
|
164
|
+
text += String.fromCharCode(b);
|
|
165
|
+
return btoa(text).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
166
|
+
}
|
|
167
|
+
function unbase64url(text) {
|
|
168
|
+
const padded = text.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (text.length % 4)) % 4);
|
|
169
|
+
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=sessions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inferlo inside SvelteKit (2 and 3) server routes, without a runtime dependency on SvelteKit:
|
|
3
|
+
*
|
|
4
|
+
* // src/routes/api/[...path]/+server.ts
|
|
5
|
+
* import { toSvelteKitHandlers } from "@browserlessapi/inferlo/sveltekit";
|
|
6
|
+
* import { app } from "$lib/server/inferlo";
|
|
7
|
+
*
|
|
8
|
+
* export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, fallback } = toSvelteKitHandlers(app, { prefix: "/api" });
|
|
9
|
+
*
|
|
10
|
+
* // src/hooks.server.ts: event.locals.ai in load functions, form actions and +server.ts files
|
|
11
|
+
* import { inferloAI } from "@browserlessapi/inferlo/sveltekit";
|
|
12
|
+
* export const handle = inferloAI(ai);
|
|
13
|
+
*
|
|
14
|
+
* It never loads the router, database or jobs code.
|
|
15
|
+
*/
|
|
16
|
+
import type { AI } from "./ai/index.ts";
|
|
17
|
+
import type { App as InferloApp } from "./app.ts";
|
|
18
|
+
declare global {
|
|
19
|
+
namespace App {
|
|
20
|
+
interface Locals {
|
|
21
|
+
/** The AI client from the inferloAI() handle hook of @browserlessapi/inferlo/sveltekit. */
|
|
22
|
+
ai: AI;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** The parts of a SvelteKit RequestEvent Inferlo uses. */
|
|
27
|
+
export interface SvelteKitEventLike {
|
|
28
|
+
/** The incoming Web request. */
|
|
29
|
+
request: Request;
|
|
30
|
+
/** The client IP from the adapter; called once per request, and errors (some adapters have none) are ignored. */
|
|
31
|
+
getClientAddress?: () => string;
|
|
32
|
+
/** event.locals, where the inferloAI() hook puts ai. */
|
|
33
|
+
locals: object;
|
|
34
|
+
}
|
|
35
|
+
/** A SvelteKit request handler, to export from a +server.ts file. */
|
|
36
|
+
export type SvelteKitRequestHandler = (event: SvelteKitEventLike) => Promise<Response>;
|
|
37
|
+
/** One request handler per HTTP method and a fallback for every other method, to export from +server.ts. */
|
|
38
|
+
export interface SvelteKitHandlers {
|
|
39
|
+
GET: SvelteKitRequestHandler;
|
|
40
|
+
POST: SvelteKitRequestHandler;
|
|
41
|
+
PUT: SvelteKitRequestHandler;
|
|
42
|
+
PATCH: SvelteKitRequestHandler;
|
|
43
|
+
DELETE: SvelteKitRequestHandler;
|
|
44
|
+
HEAD: SvelteKitRequestHandler;
|
|
45
|
+
OPTIONS: SvelteKitRequestHandler;
|
|
46
|
+
fallback: SvelteKitRequestHandler;
|
|
47
|
+
}
|
|
48
|
+
export interface SvelteKitHandlersOptions {
|
|
49
|
+
/**
|
|
50
|
+
* The URL path of the route folder, removed before the app routes the request: "/api" for
|
|
51
|
+
* src/routes/api/[...path]/+server.ts. Requests outside it keep their path. Include SvelteKit's paths.base if set.
|
|
52
|
+
*/
|
|
53
|
+
prefix?: string;
|
|
54
|
+
}
|
|
55
|
+
/** A SvelteKit handle hook: export const handle = inferloAI(ai), or sequence(inferloAI(ai), ...) to combine it. */
|
|
56
|
+
export type SvelteKitHandle = (input: {
|
|
57
|
+
event: {
|
|
58
|
+
locals: object;
|
|
59
|
+
};
|
|
60
|
+
resolve: (event: any, opts?: any) => Response | Promise<Response>;
|
|
61
|
+
}) => Response | Promise<Response>;
|
|
62
|
+
/**
|
|
63
|
+
* Request handlers that run a whole Inferlo app (routes, auth, validation, rate limits, OpenAPI and MCP) from a
|
|
64
|
+
* SvelteKit rest route: export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, fallback } = toSvelteKitHandlers(app).
|
|
65
|
+
*/
|
|
66
|
+
export declare function toSvelteKitHandlers(app: Pick<InferloApp<any>, "fetch">, options?: SvelteKitHandlersOptions): SvelteKitHandlers;
|
|
67
|
+
/**
|
|
68
|
+
* A SvelteKit handle hook that puts an AI client (createAI(...) or an Inferlo app's app.ai) on every request as
|
|
69
|
+
* event.locals.ai, for load functions, form actions and +server.ts files: export const handle = inferloAI(ai) in
|
|
70
|
+
* src/hooks.server.ts, or sequence(inferloAI(ai), yourHandle).
|
|
71
|
+
*/
|
|
72
|
+
export declare function inferloAI(ai: AI): SvelteKitHandle;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { assertAIClient } from "./ai-client.js";
|
|
2
|
+
import { clientIps } from "./client-ip.js";
|
|
3
|
+
import { InferloError } from "./errors.js";
|
|
4
|
+
import { normalizePrefix, stripPrefix } from "./serve.js";
|
|
5
|
+
const fix = 'Pass the app from createApp and the route folder\'s path: export const { GET, POST, fallback } = toSvelteKitHandlers(app, { prefix: "/api" }).';
|
|
6
|
+
/**
|
|
7
|
+
* Request handlers that run a whole Inferlo app (routes, auth, validation, rate limits, OpenAPI and MCP) from a
|
|
8
|
+
* SvelteKit rest route: export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, fallback } = toSvelteKitHandlers(app).
|
|
9
|
+
*/
|
|
10
|
+
export function toSvelteKitHandlers(app, options = {}) {
|
|
11
|
+
if (!app || typeof app.fetch !== "function") {
|
|
12
|
+
throw new InferloError("IL5022", "toSvelteKitHandlers() needs an Inferlo app (the value returned by createApp).", { fix });
|
|
13
|
+
}
|
|
14
|
+
if (options === null || typeof options !== "object" || (options.prefix !== undefined && (typeof options.prefix !== "string" || !options.prefix.startsWith("/")))) {
|
|
15
|
+
throw new InferloError("IL5022", 'toSvelteKitHandlers() prefix must be a path that starts with /, such as "/api".', { fix });
|
|
16
|
+
}
|
|
17
|
+
const prefix = normalizePrefix(options.prefix);
|
|
18
|
+
const handle = async (event) => {
|
|
19
|
+
const incoming = event?.request;
|
|
20
|
+
if (!(incoming instanceof Request)) {
|
|
21
|
+
throw new InferloError("IL5022", "toSvelteKitHandlers()'s handlers were called with something that is not a SvelteKit request event.", {
|
|
22
|
+
fix: "Export them from a +server.ts file so SvelteKit calls them: export const { GET, POST, fallback } = toSvelteKitHandlers(app, { prefix: \"/api\" }).",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
let request = incoming;
|
|
26
|
+
if (prefix) {
|
|
27
|
+
const url = new URL(incoming.url);
|
|
28
|
+
const stripped = stripPrefix(url.pathname, prefix);
|
|
29
|
+
if (stripped !== null) {
|
|
30
|
+
url.pathname = stripped;
|
|
31
|
+
const hasBody = incoming.body !== null && incoming.method !== "GET" && incoming.method !== "HEAD";
|
|
32
|
+
const init = {
|
|
33
|
+
method: incoming.method,
|
|
34
|
+
headers: incoming.headers,
|
|
35
|
+
body: hasBody ? incoming.body : undefined,
|
|
36
|
+
signal: incoming.signal,
|
|
37
|
+
redirect: incoming.redirect,
|
|
38
|
+
};
|
|
39
|
+
if (hasBody)
|
|
40
|
+
init.duplex = "half";
|
|
41
|
+
request = new Request(url, init);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const ip = event.getClientAddress?.();
|
|
46
|
+
if (typeof ip === "string" && ip)
|
|
47
|
+
clientIps.set(request, ip);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// The adapter cannot tell the client address (for example while prerendering).
|
|
51
|
+
}
|
|
52
|
+
return app.fetch(request);
|
|
53
|
+
};
|
|
54
|
+
return Object.freeze({ GET: handle, POST: handle, PUT: handle, PATCH: handle, DELETE: handle, HEAD: handle, OPTIONS: handle, fallback: handle });
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A SvelteKit handle hook that puts an AI client (createAI(...) or an Inferlo app's app.ai) on every request as
|
|
58
|
+
* event.locals.ai, for load functions, form actions and +server.ts files: export const handle = inferloAI(ai) in
|
|
59
|
+
* src/hooks.server.ts, or sequence(inferloAI(ai), yourHandle).
|
|
60
|
+
*/
|
|
61
|
+
export function inferloAI(ai) {
|
|
62
|
+
assertAIClient(ai, 'Pass createAI({ providers, models }) from "@browserlessapi/inferlo/ai", or an Inferlo app\'s app.ai: export const handle = inferloAI(ai).');
|
|
63
|
+
return ({ event, resolve }) => {
|
|
64
|
+
event.locals.ai = ai;
|
|
65
|
+
return resolve(event);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=sveltekit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sveltekit.js","sourceRoot":"","sources":["../src/sveltekit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry tracing, with no dependency: pass the OpenTelemetry API you already use.
|
|
3
|
+
*
|
|
4
|
+
* import * as otel from "@opentelemetry/api";
|
|
5
|
+
* createApp({ tracing: { api: otel } })
|
|
6
|
+
*
|
|
7
|
+
* Every request gets a server span ("GET /tickets/:id") that continues the caller's trace (W3C traceparent, through
|
|
8
|
+
* the propagator your OpenTelemetry SDK registers), runs the handler inside it so spans you or instrumentations
|
|
9
|
+
* start nest under it, and puts traceId and spanId on the request's log entries. AI calls get client spans with the
|
|
10
|
+
* model, tokens and cost (also with createAI({ tracing }) on its own), and database queries made while a request
|
|
11
|
+
* or job is traced get client spans with the table and operation. Without an SDK, the API's no-op tracer is used
|
|
12
|
+
* and nothing is recorded.
|
|
13
|
+
*/
|
|
14
|
+
import type { Adapter } from "./db/types.ts";
|
|
15
|
+
/** The parts of a span Inferlo uses (an @opentelemetry/api Span). */
|
|
16
|
+
export interface OtelSpan {
|
|
17
|
+
spanContext(): {
|
|
18
|
+
traceId: string;
|
|
19
|
+
spanId: string;
|
|
20
|
+
traceFlags: number;
|
|
21
|
+
};
|
|
22
|
+
setAttribute(key: string, value: string | number | boolean): unknown;
|
|
23
|
+
setStatus(status: {
|
|
24
|
+
code: number;
|
|
25
|
+
message?: string;
|
|
26
|
+
}): unknown;
|
|
27
|
+
recordException(exception: unknown): void;
|
|
28
|
+
updateName(name: string): unknown;
|
|
29
|
+
end(): void;
|
|
30
|
+
isRecording(): boolean;
|
|
31
|
+
}
|
|
32
|
+
/** The parts of @opentelemetry/api Inferlo uses: import * as otel from "@opentelemetry/api", then { api: otel }. */
|
|
33
|
+
export interface OtelApi {
|
|
34
|
+
trace: {
|
|
35
|
+
getTracer(name: string, version?: string): {
|
|
36
|
+
startSpan(name: string, options?: {
|
|
37
|
+
kind?: number;
|
|
38
|
+
attributes?: Record<string, string | number | boolean>;
|
|
39
|
+
}, context?: unknown): OtelSpan;
|
|
40
|
+
};
|
|
41
|
+
setSpan(context: unknown, span: OtelSpan): unknown;
|
|
42
|
+
/** The span of a context. Database spans are only started inside one (a request or a job). */
|
|
43
|
+
getSpan?(context: unknown): OtelSpan | undefined;
|
|
44
|
+
};
|
|
45
|
+
context: {
|
|
46
|
+
active(): unknown;
|
|
47
|
+
with<T>(context: unknown, fn: () => T): T;
|
|
48
|
+
};
|
|
49
|
+
propagation: {
|
|
50
|
+
extract(context: unknown, carrier: unknown, getter?: unknown): unknown;
|
|
51
|
+
/** Writes the trace of a context into a carrier (traceparent), so a queued job continues it. */
|
|
52
|
+
inject?(context: unknown, carrier: unknown, setter?: unknown): void;
|
|
53
|
+
};
|
|
54
|
+
SpanKind: {
|
|
55
|
+
SERVER: number;
|
|
56
|
+
CLIENT?: number;
|
|
57
|
+
CONSUMER?: number;
|
|
58
|
+
};
|
|
59
|
+
SpanStatusCode: {
|
|
60
|
+
ERROR: number;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export interface TracingConfig {
|
|
64
|
+
/** The OpenTelemetry API: import * as otel from "@opentelemetry/api", then createApp({ tracing: { api: otel } }). */
|
|
65
|
+
api: OtelApi;
|
|
66
|
+
/** The tracer name. Default: the app's name (createApp), or "inferlo" (createAI on its own). */
|
|
67
|
+
tracerName?: string;
|
|
68
|
+
}
|
|
69
|
+
type Attributes = Record<string, string | number | boolean>;
|
|
70
|
+
export interface RequestTracing {
|
|
71
|
+
/** Starts the server span of a request and runs fn inside it. */
|
|
72
|
+
run<T>(req: Request, url: URL, fn: (span: OtelSpan) => Promise<T>): Promise<T>;
|
|
73
|
+
}
|
|
74
|
+
/** Checks a tracing option (createApp or createAI) and returns it, or undefined without tracing. */
|
|
75
|
+
export declare function checkTracing(option: unknown, where: "createApp" | "createAI"): TracingConfig | undefined;
|
|
76
|
+
/** Checks createApp({ tracing }) and returns the request tracer, or undefined without tracing. */
|
|
77
|
+
export declare function requestTracing(option: unknown, appName: string, appVersion: string): RequestTracing | undefined;
|
|
78
|
+
/** Client spans for calls Inferlo makes: AI models and the database. */
|
|
79
|
+
export interface ClientTracing {
|
|
80
|
+
/** Starts a client span under the active one. The caller ends it (with end or fail). */
|
|
81
|
+
start(name: string, attributes: Attributes): OtelSpan;
|
|
82
|
+
/** Runs fn inside a client span that is active while fn runs, and ends it; errors are recorded on it. */
|
|
83
|
+
run<T>(name: string, attributes: Attributes, fn: (span: OtelSpan) => Promise<T>): Promise<T>;
|
|
84
|
+
/** Records an error on a span and ends it. */
|
|
85
|
+
fail(span: OtelSpan, error: unknown): void;
|
|
86
|
+
/** True while a span (a request's or a job's) is active. */
|
|
87
|
+
inSpan(): boolean;
|
|
88
|
+
}
|
|
89
|
+
export declare function clientTracing(config: TracingConfig, defaultName: string, version?: string): ClientTracing;
|
|
90
|
+
/**
|
|
91
|
+
* Wraps a database adapter so its queries get client spans ("SELECT tickets") while a request or job is traced.
|
|
92
|
+
* Queries outside a trace (startup, the job worker polling for work) are not traced. Values are never recorded,
|
|
93
|
+
* only the parameterised SQL of raw queries.
|
|
94
|
+
*/
|
|
95
|
+
export declare function traceAdapter(adapter: Adapter, tracing: ClientTracing): Adapter;
|
|
96
|
+
/** traceId and spanId for log entries, when the span belongs to a real trace. */
|
|
97
|
+
export declare function traceFields(span: OtelSpan | undefined): {
|
|
98
|
+
traceId?: string;
|
|
99
|
+
spanId?: string;
|
|
100
|
+
};
|
|
101
|
+
/** The trace a queued job carries from where it was enqueued (W3C trace context). */
|
|
102
|
+
export interface JobTraceContext {
|
|
103
|
+
traceparent?: string;
|
|
104
|
+
tracestate?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface JobTracing {
|
|
107
|
+
/** The trace of the active span, to store with a job that is enqueued now. Empty outside a trace. */
|
|
108
|
+
capture(): JobTraceContext;
|
|
109
|
+
/** Runs one attempt of a job in a span that continues the trace it was enqueued in (cron jobs start a new trace). */
|
|
110
|
+
run<T extends string>(job: {
|
|
111
|
+
id: string;
|
|
112
|
+
name: string;
|
|
113
|
+
attempts: number;
|
|
114
|
+
trace?: JobTraceContext;
|
|
115
|
+
}, fn: (fail: (error: unknown) => void) => Promise<T>): Promise<T>;
|
|
116
|
+
}
|
|
117
|
+
export declare function jobTracing(config: TracingConfig, defaultName: string, version?: string): JobTracing;
|
|
118
|
+
export {};
|
package/dist/tracing.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { InferloError } from "./errors.js";
|
|
2
|
+
/** CLIENT and CONSUMER in @opentelemetry/api's SpanKind (the same in every 1.x release). */
|
|
3
|
+
const CLIENT_KIND = 2;
|
|
4
|
+
const CONSUMER_KIND = 4;
|
|
5
|
+
/** Reads incoming trace headers (traceparent, tracestate, baggage) for the propagator. */
|
|
6
|
+
const headerGetter = {
|
|
7
|
+
get(carrier, key) {
|
|
8
|
+
return carrier.get(key) ?? undefined;
|
|
9
|
+
},
|
|
10
|
+
keys(carrier) {
|
|
11
|
+
return [...carrier.keys()];
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const INVALID_TRACE = "00000000000000000000000000000000";
|
|
15
|
+
/** Checks a tracing option (createApp or createAI) and returns it, or undefined without tracing. */
|
|
16
|
+
export function checkTracing(option, where) {
|
|
17
|
+
if (option === undefined || option === false)
|
|
18
|
+
return undefined;
|
|
19
|
+
const config = option;
|
|
20
|
+
const api = config?.api;
|
|
21
|
+
const valid = config !== null &&
|
|
22
|
+
typeof config === "object" &&
|
|
23
|
+
typeof api?.trace?.getTracer === "function" &&
|
|
24
|
+
typeof api.trace.setSpan === "function" &&
|
|
25
|
+
typeof api.context?.with === "function" &&
|
|
26
|
+
typeof api.context.active === "function" &&
|
|
27
|
+
typeof api.propagation?.extract === "function" &&
|
|
28
|
+
typeof api.SpanKind?.SERVER === "number" &&
|
|
29
|
+
typeof api.SpanStatusCode?.ERROR === "number" &&
|
|
30
|
+
(config.tracerName === undefined || (typeof config.tracerName === "string" && config.tracerName.length > 0));
|
|
31
|
+
if (!valid) {
|
|
32
|
+
throw new InferloError("IL6030", "Invalid tracing options: tracing needs { api } with the OpenTelemetry API.", {
|
|
33
|
+
fix: `Install @opentelemetry/api (and an OpenTelemetry SDK that exports the spans), then: import * as otel from "@opentelemetry/api"; ${where}({ tracing: { api: otel } }).`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
/** Checks createApp({ tracing }) and returns the request tracer, or undefined without tracing. */
|
|
39
|
+
export function requestTracing(option, appName, appVersion) {
|
|
40
|
+
const config = checkTracing(option, "createApp");
|
|
41
|
+
if (!config)
|
|
42
|
+
return undefined;
|
|
43
|
+
const api = config.api;
|
|
44
|
+
const tracer = api.trace.getTracer(config.tracerName ?? appName, appVersion);
|
|
45
|
+
return {
|
|
46
|
+
run(req, url, fn) {
|
|
47
|
+
const method = req.method.toUpperCase();
|
|
48
|
+
const parent = api.propagation.extract(api.context.active(), req.headers, headerGetter);
|
|
49
|
+
const attributes = {
|
|
50
|
+
"http.request.method": method,
|
|
51
|
+
"url.path": url.pathname,
|
|
52
|
+
"url.scheme": url.protocol.replace(/:$/, ""),
|
|
53
|
+
"server.address": url.hostname,
|
|
54
|
+
};
|
|
55
|
+
if (url.port)
|
|
56
|
+
attributes["server.port"] = Number(url.port);
|
|
57
|
+
const agent = req.headers.get("user-agent");
|
|
58
|
+
if (agent)
|
|
59
|
+
attributes["user_agent.original"] = agent;
|
|
60
|
+
const span = tracer.startSpan(method, { kind: api.SpanKind.SERVER, attributes }, parent);
|
|
61
|
+
return api.context.with(api.trace.setSpan(parent, span), () => fn(span));
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function clientTracing(config, defaultName, version) {
|
|
66
|
+
const api = config.api;
|
|
67
|
+
const tracer = api.trace.getTracer(config.tracerName ?? defaultName, version);
|
|
68
|
+
const kind = api.SpanKind.CLIENT ?? CLIENT_KIND;
|
|
69
|
+
const fail = (span, error) => {
|
|
70
|
+
span.recordException(error instanceof Error ? error : String(error));
|
|
71
|
+
const code = error?.code;
|
|
72
|
+
if (typeof code === "string")
|
|
73
|
+
span.setAttribute("error.type", code);
|
|
74
|
+
else
|
|
75
|
+
span.setAttribute("error.type", error instanceof Error ? error.name : "Error");
|
|
76
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
77
|
+
span.end();
|
|
78
|
+
};
|
|
79
|
+
const start = (name, attributes) => tracer.startSpan(name, { kind, attributes }, api.context.active());
|
|
80
|
+
return {
|
|
81
|
+
start,
|
|
82
|
+
fail,
|
|
83
|
+
async run(name, attributes, fn) {
|
|
84
|
+
const span = start(name, attributes);
|
|
85
|
+
let result;
|
|
86
|
+
try {
|
|
87
|
+
result = await api.context.with(api.trace.setSpan(api.context.active(), span), () => fn(span));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
fail(span, error);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
span.end();
|
|
94
|
+
return result;
|
|
95
|
+
},
|
|
96
|
+
inSpan() {
|
|
97
|
+
if (!api.trace.getSpan)
|
|
98
|
+
return true;
|
|
99
|
+
const active = api.trace.getSpan(api.context.active());
|
|
100
|
+
return active !== undefined && active.spanContext().traceId !== INVALID_TRACE;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const SQL_OPERATION = /^\s*([a-z]+)/i;
|
|
105
|
+
/**
|
|
106
|
+
* Wraps a database adapter so its queries get client spans ("SELECT tickets") while a request or job is traced.
|
|
107
|
+
* Queries outside a trace (startup, the job worker polling for work) are not traced. Values are never recorded,
|
|
108
|
+
* only the parameterised SQL of raw queries.
|
|
109
|
+
*/
|
|
110
|
+
export function traceAdapter(adapter, tracing) {
|
|
111
|
+
const system = adapter.dialect === "postgres" ? "postgresql" : adapter.dialect;
|
|
112
|
+
const traced = (operation, table, query, fn) => {
|
|
113
|
+
if (!tracing.inSpan())
|
|
114
|
+
return fn();
|
|
115
|
+
const attributes = { "db.system.name": system, "db.operation.name": operation };
|
|
116
|
+
if (table)
|
|
117
|
+
attributes["db.collection.name"] = table;
|
|
118
|
+
if (query)
|
|
119
|
+
attributes["db.query.text"] = query;
|
|
120
|
+
return tracing.run(table ? `${operation} ${table}` : operation, attributes, fn);
|
|
121
|
+
};
|
|
122
|
+
const wrapped = {
|
|
123
|
+
...adapter,
|
|
124
|
+
insert: (model, row) => traced("INSERT", model.name, undefined, () => adapter.insert(model, row)),
|
|
125
|
+
select: (model, query) => traced("SELECT", model.name, undefined, () => adapter.select(model, query)),
|
|
126
|
+
update: (model, where, patch) => traced("UPDATE", model.name, undefined, () => adapter.update(model, where, patch)),
|
|
127
|
+
delete: (model, where) => traced("DELETE", model.name, undefined, () => adapter.delete(model, where)),
|
|
128
|
+
count: (model, where) => traced("COUNT", model.name, undefined, () => adapter.count(model, where)),
|
|
129
|
+
};
|
|
130
|
+
if (adapter.transaction)
|
|
131
|
+
wrapped.transaction = (fn) => adapter.transaction((tx) => fn(traceAdapter(tx, tracing)));
|
|
132
|
+
if (adapter.sql) {
|
|
133
|
+
wrapped.sql = (text, params) => traced((SQL_OPERATION.exec(text)?.[1] ?? "SQL").toUpperCase(), undefined, text, () => adapter.sql(text, params));
|
|
134
|
+
}
|
|
135
|
+
return wrapped;
|
|
136
|
+
}
|
|
137
|
+
/** traceId and spanId for log entries, when the span belongs to a real trace. */
|
|
138
|
+
export function traceFields(span) {
|
|
139
|
+
const context = span?.spanContext();
|
|
140
|
+
if (!context || context.traceId === INVALID_TRACE)
|
|
141
|
+
return {};
|
|
142
|
+
return { traceId: context.traceId, spanId: context.spanId };
|
|
143
|
+
}
|
|
144
|
+
const carrierGetter = {
|
|
145
|
+
get(carrier, key) {
|
|
146
|
+
return key === "traceparent" || key === "tracestate" ? carrier[key] : undefined;
|
|
147
|
+
},
|
|
148
|
+
keys(carrier) {
|
|
149
|
+
return Object.keys(carrier);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const TRACEPARENT = /^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/;
|
|
153
|
+
export function jobTracing(config, defaultName, version) {
|
|
154
|
+
const api = config.api;
|
|
155
|
+
const tracer = api.trace.getTracer(config.tracerName ?? defaultName, version);
|
|
156
|
+
const kind = api.SpanKind.CONSUMER ?? CONSUMER_KIND;
|
|
157
|
+
return {
|
|
158
|
+
capture() {
|
|
159
|
+
if (!api.propagation.inject)
|
|
160
|
+
return {};
|
|
161
|
+
const carrier = {};
|
|
162
|
+
api.propagation.inject(api.context.active(), carrier);
|
|
163
|
+
const trace = {};
|
|
164
|
+
if (typeof carrier.traceparent === "string" && TRACEPARENT.test(carrier.traceparent)) {
|
|
165
|
+
trace.traceparent = carrier.traceparent;
|
|
166
|
+
if (typeof carrier.tracestate === "string" && carrier.tracestate.length <= 512)
|
|
167
|
+
trace.tracestate = carrier.tracestate;
|
|
168
|
+
}
|
|
169
|
+
return trace;
|
|
170
|
+
},
|
|
171
|
+
async run(job, fn) {
|
|
172
|
+
const parent = job.trace?.traceparent ? api.propagation.extract(api.context.active(), job.trace, carrierGetter) : api.context.active();
|
|
173
|
+
const span = tracer.startSpan(`process ${job.name}`, {
|
|
174
|
+
kind,
|
|
175
|
+
attributes: {
|
|
176
|
+
"messaging.system": "inferlo",
|
|
177
|
+
"messaging.operation.type": "process",
|
|
178
|
+
"messaging.destination.name": job.name,
|
|
179
|
+
"messaging.message.id": job.id,
|
|
180
|
+
"inferlo.job.attempt": job.attempts,
|
|
181
|
+
},
|
|
182
|
+
}, parent);
|
|
183
|
+
const fail = (error) => {
|
|
184
|
+
span.recordException(error instanceof Error ? error : String(error));
|
|
185
|
+
const code = error?.code;
|
|
186
|
+
span.setAttribute("error.type", typeof code === "string" ? code : error instanceof Error ? error.name : "Error");
|
|
187
|
+
span.setStatus({ code: api.SpanStatusCode.ERROR });
|
|
188
|
+
};
|
|
189
|
+
try {
|
|
190
|
+
const outcome = await api.context.with(api.trace.setSpan(parent, span), () => fn(fail));
|
|
191
|
+
span.setAttribute("inferlo.job.outcome", outcome);
|
|
192
|
+
return outcome;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
fail(error);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
span.end();
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
//# sourceMappingURL=tracing.js.map
|