silgi 0.1.0-beta.12 → 0.1.0-beta.14
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/dist/client/adapters/fetch/index.d.mts +0 -4
- package/dist/client/adapters/fetch/index.mjs +3 -28
- package/dist/client/adapters/ofetch/index.d.mts +0 -2
- package/dist/client/adapters/ofetch/index.mjs +2 -17
- package/dist/client/openapi.mjs +1 -1
- package/dist/client/server.mjs +1 -4
- package/dist/core/context-bridge.mjs +11 -0
- package/dist/core/handler.mjs +2 -1
- package/dist/core/router-utils.mjs +1 -82
- package/dist/core/serve.d.mts +2 -2
- package/dist/core/serve.mjs +2 -2
- package/dist/index.d.mts +1 -2
- package/dist/index.mjs +1 -2
- package/dist/integrations/better-auth/index.d.mts +0 -20
- package/dist/integrations/better-auth/index.mjs +3 -4
- package/dist/integrations/drizzle/index.mjs +6 -7
- package/dist/plugins/analytics.d.mts +14 -8
- package/dist/plugins/analytics.mjs +404 -57
- package/dist/scalar.mjs +4 -4
- package/dist/silgi.d.mts +1 -1
- package/lib/dashboard/index.html +53 -56
- package/package.json +1 -5
- package/dist/contract.d.mts +0 -87
- package/dist/contract.mjs +0 -129
- package/dist/core/router-utils.d.mts +0 -25
|
@@ -5,10 +5,6 @@ interface RPCLinkOptions<TClientContext extends ClientContext = ClientContext> {
|
|
|
5
5
|
url: string | URL;
|
|
6
6
|
headers?: Record<string, string> | ((options: ClientOptions<TClientContext>) => Record<string, string>);
|
|
7
7
|
fetch?: typeof globalThis.fetch;
|
|
8
|
-
method?: 'GET' | 'POST';
|
|
9
|
-
maxUrlLength?: number;
|
|
10
|
-
/** Route metadata — pass extractRoutes(router) or the full router. Required when procedures use $route({ path }) */
|
|
11
|
-
routes?: unknown;
|
|
12
8
|
}
|
|
13
9
|
declare class RPCLink<TClientContext extends ClientContext = ClientContext> implements ClientLink<TClientContext> {
|
|
14
10
|
#private;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SilgiError, fromSilgiErrorJSON, isErrorStatus, isSilgiErrorJSON } from "../../../core/error.mjs";
|
|
2
|
-
import { resolveRoute, substituteParams } from "../../../core/router-utils.mjs";
|
|
3
2
|
import { parseEmptyableJSON, stringifyJSON } from "../../../core/utils.mjs";
|
|
4
3
|
//#region src/client/adapters/fetch/index.ts
|
|
5
4
|
/**
|
|
@@ -9,46 +8,22 @@ var RPCLink = class {
|
|
|
9
8
|
#baseUrl;
|
|
10
9
|
#headers;
|
|
11
10
|
#fetch;
|
|
12
|
-
#method;
|
|
13
|
-
#maxUrlLength;
|
|
14
|
-
#routes;
|
|
15
11
|
constructor(options) {
|
|
16
12
|
this.#baseUrl = typeof options.url === "string" ? options.url : options.url.href;
|
|
17
13
|
if (this.#baseUrl.endsWith("/")) this.#baseUrl = this.#baseUrl.slice(0, -1);
|
|
18
14
|
this.#headers = options.headers;
|
|
19
15
|
this.#fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
20
|
-
this.#method = options.method ?? "POST";
|
|
21
|
-
this.#maxUrlLength = options.maxUrlLength ?? 2083;
|
|
22
|
-
this.#routes = options.routes;
|
|
23
16
|
}
|
|
24
17
|
async call(path, input, options) {
|
|
25
|
-
const
|
|
26
|
-
let urlPath = resolved ? resolved.path : "/" + path.map(encodeURIComponent).join("/");
|
|
27
|
-
if (resolved) {
|
|
28
|
-
const sub = substituteParams(urlPath, input);
|
|
29
|
-
urlPath = sub.url;
|
|
30
|
-
input = sub.remainingInput;
|
|
31
|
-
}
|
|
32
|
-
let url = `${this.#baseUrl}${urlPath}`;
|
|
18
|
+
const url = `${this.#baseUrl}/${path.map(encodeURIComponent).join("/")}`;
|
|
33
19
|
const headers = { ...typeof this.#headers === "function" ? this.#headers(options) : this.#headers };
|
|
34
|
-
let method = resolved?.method ?? this.#method;
|
|
35
20
|
let body;
|
|
36
|
-
|
|
37
|
-
if (method === "GET" && hasInput) {
|
|
38
|
-
const data = stringifyJSON(input);
|
|
39
|
-
const candidateUrl = `${url}?data=${encodeURIComponent(data)}`;
|
|
40
|
-
if (candidateUrl.length <= this.#maxUrlLength) url = candidateUrl;
|
|
41
|
-
else {
|
|
42
|
-
method = "POST";
|
|
43
|
-
headers["content-type"] = "application/json";
|
|
44
|
-
body = data;
|
|
45
|
-
}
|
|
46
|
-
} else if (hasInput) {
|
|
21
|
+
if (input !== void 0 && input !== null) {
|
|
47
22
|
headers["content-type"] = "application/json";
|
|
48
23
|
body = stringifyJSON(input);
|
|
49
24
|
}
|
|
50
25
|
const response = await this.#fetch(url, {
|
|
51
|
-
method,
|
|
26
|
+
method: "POST",
|
|
52
27
|
headers,
|
|
53
28
|
body,
|
|
54
29
|
signal: options.signal
|
|
@@ -36,8 +36,6 @@ interface LinkOptions<TClientContext extends ClientContext = ClientContext> {
|
|
|
36
36
|
onResponse?: FetchOptions['onResponse'];
|
|
37
37
|
onRequestError?: FetchOptions['onRequestError'];
|
|
38
38
|
onResponseError?: FetchOptions['onResponseError'];
|
|
39
|
-
/** Route metadata — pass extractRoutes(router), the full router, or a Promise that resolves to routes. Required when procedures use $route({ path }) */
|
|
40
|
-
routes?: unknown | Promise<unknown>;
|
|
41
39
|
}
|
|
42
40
|
/**
|
|
43
41
|
* Create a Silgi client link powered by ofetch.
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SilgiError, fromSilgiErrorJSON, isSilgiErrorJSON } from "../../../core/error.mjs";
|
|
2
|
-
import { resolveRoute, substituteParams } from "../../../core/router-utils.mjs";
|
|
3
2
|
import { MSGPACK_CONTENT_TYPE, decode, encode } from "../../../codec/msgpack.mjs";
|
|
4
3
|
import { FetchError, ofetch } from "ofetch";
|
|
5
4
|
//#region src/client/adapters/ofetch/index.ts
|
|
@@ -28,22 +27,8 @@ function createLink(options) {
|
|
|
28
27
|
const defaultRetry = options.retry;
|
|
29
28
|
const defaultRetryDelay = options.retryDelay ?? 0;
|
|
30
29
|
const resolvedProtocol = options.protocol ?? (options.binary ? "messagepack" : void 0) ?? (options.devalue ? "devalue" : void 0) ?? "json";
|
|
31
|
-
let resolvedRoutes = void 0;
|
|
32
|
-
const routesOption = options.routes;
|
|
33
|
-
if (routesOption && typeof routesOption?.then === "function") routesOption.then((r) => {
|
|
34
|
-
resolvedRoutes = r;
|
|
35
|
-
});
|
|
36
|
-
else resolvedRoutes = routesOption;
|
|
37
30
|
return { async call(path, input, callOptions) {
|
|
38
|
-
|
|
39
|
-
const resolved = resolvedRoutes ? resolveRoute(resolvedRoutes, path) : void 0;
|
|
40
|
-
let urlPath = resolved ? resolved.path : "/" + path.map(encodeURIComponent).join("/");
|
|
41
|
-
if (resolved) {
|
|
42
|
-
const sub = substituteParams(urlPath, input);
|
|
43
|
-
urlPath = sub.url;
|
|
44
|
-
input = sub.remainingInput;
|
|
45
|
-
}
|
|
46
|
-
const url = `${baseUrl}${urlPath}`;
|
|
31
|
+
const url = `${baseUrl}/${path.map(encodeURIComponent).join("/")}`;
|
|
47
32
|
const headers = { ...typeof options.headers === "function" ? options.headers(callOptions) : options.headers };
|
|
48
33
|
let body;
|
|
49
34
|
if (resolvedProtocol === "messagepack") {
|
|
@@ -58,7 +43,7 @@ function createLink(options) {
|
|
|
58
43
|
} else body = input !== void 0 && input !== null ? input : void 0;
|
|
59
44
|
try {
|
|
60
45
|
const data = await ofetch(url, {
|
|
61
|
-
method:
|
|
46
|
+
method: "POST",
|
|
62
47
|
headers,
|
|
63
48
|
body,
|
|
64
49
|
signal: callOptions.signal,
|
package/dist/client/openapi.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { SilgiError } from "../core/error.mjs";
|
|
|
13
13
|
*
|
|
14
14
|
* const link = new OpenAPILink({
|
|
15
15
|
* url: "https://api.example.com",
|
|
16
|
-
* spec: await fetch("/openapi.json").then(r => r.json()),
|
|
16
|
+
* spec: await fetch("/api/openapi.json").then(r => r.json()),
|
|
17
17
|
* })
|
|
18
18
|
*
|
|
19
19
|
* const client = createClient<ExternalAPI>(link)
|
package/dist/client/server.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { resolveRoute } from "../core/router-utils.mjs";
|
|
2
1
|
import { compileRouter } from "../compile.mjs";
|
|
3
2
|
//#region src/client/server.ts
|
|
4
3
|
/**
|
|
@@ -31,9 +30,7 @@ function createServerClient(router, options) {
|
|
|
31
30
|
function createServerProxy(router, flatRouter, contextFactory, path) {
|
|
32
31
|
const cache = /* @__PURE__ */ new Map();
|
|
33
32
|
const callProcedure = async (input) => {
|
|
34
|
-
const
|
|
35
|
-
const routePath = resolved?.path ?? "/" + path.join("/");
|
|
36
|
-
const route = flatRouter(resolved?.method ?? "POST", routePath)?.data;
|
|
33
|
+
const route = flatRouter("POST", "/" + path.join("/"))?.data;
|
|
37
34
|
if (!route) throw new Error(`Procedure not found: ${path.join("/")}`);
|
|
38
35
|
const ctx = Object.create(null);
|
|
39
36
|
const baseCtx = await contextFactory();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
//#region src/core/context-bridge.ts
|
|
3
|
+
const ctxStorage = new AsyncLocalStorage();
|
|
4
|
+
function runWithCtx(ctx, fn) {
|
|
5
|
+
return ctxStorage.run(ctx, fn);
|
|
6
|
+
}
|
|
7
|
+
function getCtx() {
|
|
8
|
+
return ctxStorage.getStore();
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { getCtx, runWithCtx };
|
package/dist/core/handler.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { compileRouter, createContext, releaseContext } from "../compile.mjs";
|
|
|
3
3
|
import { applyContext } from "./dispatch.mjs";
|
|
4
4
|
import { analyticsTraceMap } from "../plugins/analytics.mjs";
|
|
5
5
|
import { detectResponseFormat, encodeResponse, makeErrorResponse } from "./codec.mjs";
|
|
6
|
+
import { runWithCtx } from "./context-bridge.mjs";
|
|
6
7
|
import { parseInput } from "./input.mjs";
|
|
7
8
|
import { iteratorToEventStream } from "./sse.mjs";
|
|
8
9
|
//#region src/core/handler.ts
|
|
@@ -128,7 +129,7 @@ function createFetchHandler(routerDef, contextFactory, hooks) {
|
|
|
128
129
|
path: pathname,
|
|
129
130
|
input: rawInput
|
|
130
131
|
});
|
|
131
|
-
const pipelineResult = route.handler(ctx, rawInput, request.signal);
|
|
132
|
+
const pipelineResult = runWithCtx(ctx, () => route.handler(ctx, rawInput, request.signal));
|
|
132
133
|
const output = pipelineResult instanceof Promise ? await pipelineResult : pipelineResult;
|
|
133
134
|
callHook("response", {
|
|
134
135
|
path: pathname,
|
|
@@ -18,86 +18,5 @@ function assignPaths(def, prefix = []) {
|
|
|
18
18
|
}
|
|
19
19
|
return result;
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Extract route metadata from a router definition.
|
|
23
|
-
* Returns a lightweight nested object safe for client bundles — no resolve functions,
|
|
24
|
-
* no guards, no schemas, no server code.
|
|
25
|
-
*
|
|
26
|
-
* ```ts
|
|
27
|
-
* // server/routes.ts — import this on the client
|
|
28
|
-
* export const routes = extractRoutes(appRouter)
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
function extractRoutes(def, prefix = []) {
|
|
32
|
-
const result = {};
|
|
33
|
-
if (def == null || typeof def !== "object") return result;
|
|
34
|
-
for (const [key, value] of Object.entries(def)) {
|
|
35
|
-
const route = getRouteFromEntry(value);
|
|
36
|
-
if (route?.path) result[key] = {
|
|
37
|
-
path: route.path,
|
|
38
|
-
method: (route.method ?? "POST").toUpperCase()
|
|
39
|
-
};
|
|
40
|
-
else if (typeof value === "object" && value !== null && !isProcedureDef(value)) {
|
|
41
|
-
const nested = extractRoutes(value, [...prefix, key]);
|
|
42
|
-
if (Object.keys(nested).length > 0) result[key] = nested;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
/** Extract route metadata from a ProcedureDef or ProcedureContract */
|
|
48
|
-
function getRouteFromEntry(value) {
|
|
49
|
-
if (typeof value !== "object" || value === null) return void 0;
|
|
50
|
-
const obj = value;
|
|
51
|
-
if (isProcedureDef(value)) return obj.route;
|
|
52
|
-
if ("route" in obj && typeof obj.route === "object" && obj.route !== null) return obj.route;
|
|
53
|
-
}
|
|
54
|
-
/** Check if a value is an extracted route leaf (has path + method, not a nested object) */
|
|
55
|
-
function isExtractedRoute(value) {
|
|
56
|
-
return typeof value === "object" && value !== null && "path" in value && "method" in value && !("resolve" in value);
|
|
57
|
-
}
|
|
58
|
-
/** Walk a route tree by path segments and return the route metadata. Works with both full routers and extracted routes. */
|
|
59
|
-
function resolveRoute(routes, path) {
|
|
60
|
-
let current = routes;
|
|
61
|
-
for (const segment of path) {
|
|
62
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
63
|
-
current = current[segment];
|
|
64
|
-
}
|
|
65
|
-
if (isExtractedRoute(current)) return current;
|
|
66
|
-
const route = getRouteFromEntry(current);
|
|
67
|
-
if (route?.path) return {
|
|
68
|
-
path: route.path,
|
|
69
|
-
method: (route.method ?? "POST").toUpperCase()
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Substitute :param placeholders in a route path with values from input.
|
|
74
|
-
* Returns the resolved URL path and the input with used params removed.
|
|
75
|
-
*/
|
|
76
|
-
function substituteParams(routePath, input) {
|
|
77
|
-
if (input == null || typeof input !== "object" || Array.isArray(input)) return {
|
|
78
|
-
url: routePath,
|
|
79
|
-
remainingInput: input
|
|
80
|
-
};
|
|
81
|
-
const obj = input;
|
|
82
|
-
const used = /* @__PURE__ */ new Set();
|
|
83
|
-
const url = routePath.replace(/:([a-zA-Z_]\w*)/g, (_match, name) => {
|
|
84
|
-
const val = obj[name];
|
|
85
|
-
if (val !== void 0) {
|
|
86
|
-
used.add(name);
|
|
87
|
-
return encodeURIComponent(String(val));
|
|
88
|
-
}
|
|
89
|
-
return `:${name}`;
|
|
90
|
-
});
|
|
91
|
-
if (used.size === 0) return {
|
|
92
|
-
url,
|
|
93
|
-
remainingInput: input
|
|
94
|
-
};
|
|
95
|
-
const remaining = {};
|
|
96
|
-
for (const key of Object.keys(obj)) if (!used.has(key)) remaining[key] = obj[key];
|
|
97
|
-
return {
|
|
98
|
-
url,
|
|
99
|
-
remainingInput: Object.keys(remaining).length > 0 ? remaining : void 0
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
21
|
//#endregion
|
|
103
|
-
export { assignPaths,
|
|
22
|
+
export { assignPaths, isProcedureDef, routerCache };
|
package/dist/core/serve.d.mts
CHANGED
|
@@ -22,9 +22,9 @@ interface SilgiServer {
|
|
|
22
22
|
interface ServeOptions {
|
|
23
23
|
port?: number;
|
|
24
24
|
hostname?: string;
|
|
25
|
-
/** Enable Scalar API Reference UI at /reference and /openapi.json */
|
|
25
|
+
/** Enable Scalar API Reference UI at /api/reference and /api/openapi.json */
|
|
26
26
|
scalar?: boolean | ScalarOptions;
|
|
27
|
-
/** Enable analytics dashboard at /analytics */
|
|
27
|
+
/** Enable analytics dashboard at /api/analytics */
|
|
28
28
|
analytics?: boolean | AnalyticsOptions;
|
|
29
29
|
/** Enable WebSocket RPC (requires crossws) */
|
|
30
30
|
ws?: boolean | WSAdapterOptions;
|
package/dist/core/serve.mjs
CHANGED
|
@@ -50,8 +50,8 @@ async function createServeHandler(routerDef, contextFactory, hooks, options) {
|
|
|
50
50
|
console.log(`\nSilgi server running at ${url}`);
|
|
51
51
|
if (options?.http2) console.log(` HTTP/2 enabled (with HTTP/1.1 fallback)`);
|
|
52
52
|
if (options?.ws) console.log(` WebSocket RPC at ws://${hostname}:${resolvedPort}`);
|
|
53
|
-
if (options?.scalar) console.log(` Scalar API Reference at ${url}/reference`);
|
|
54
|
-
if (options?.analytics) console.log(` Analytics dashboard at ${url}/analytics`);
|
|
53
|
+
if (options?.scalar) console.log(` Scalar API Reference at ${url}/api/reference`);
|
|
54
|
+
if (options?.analytics) console.log(` Analytics dashboard at ${url}/api/analytics`);
|
|
55
55
|
console.log();
|
|
56
56
|
await hooks.callHook("serve:start", {
|
|
57
57
|
url,
|
package/dist/index.d.mts
CHANGED
|
@@ -13,6 +13,5 @@ import { CallableOptions, callable } from "./callable.mjs";
|
|
|
13
13
|
import { LifecycleHooks, lifecycleWrap } from "./lifecycle.mjs";
|
|
14
14
|
import { mapInput } from "./map-input.mjs";
|
|
15
15
|
import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
|
|
16
|
-
import { ExtractedRoute, ExtractedRoutes, extractRoutes } from "./core/router-utils.mjs";
|
|
17
16
|
import { LazyRouter, isLazy, lazy, resolveLazy } from "./lazy.mjs";
|
|
18
|
-
export { type AnySchema, AsyncIteratorClass, type CallableOptions, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type
|
|
17
|
+
export { type AnySchema, AsyncIteratorClass, type CallableOptions, type Driver, type ErrorDef, type ErrorDefItem, type EventMeta, type FailFn, type GuardDef, type GuardFn, type InferClient, type InferContextFromUse, type InferGuardOutput, type InferSchemaInput, type InferSchemaOutput, type LazyRouter, type LifecycleHooks, type Meta, type MiddlewareDef, type ProcedureBuilder, type ProcedureBuilderWithOutput, type ProcedureDef, type ProcedureType, type ResolveContext, type RouterDef, type ScalarOptions, type ScheduledTaskInfo, type Schema, type ServeOptions, type SilgiConfig, SilgiError, type SilgiErrorCode, type SilgiErrorJSON, type SilgiErrorOptions, type SilgiInstance, type SilgiServer, type Storage, type StorageConfig, type StorageValue, type TaskDef, type TaskEvent, ValidationError, type WrapDef, type WrapFn, callable, collectCronTasks, compileProcedure, compileRouter, createContext, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ValidationError, type, validateSchema } from "./core/schema.mjs";
|
|
2
2
|
import { collectCronTasks, getScheduledTasks, runTask, setTaskAnalytics, startCronJobs, stopCronJobs } from "./core/task.mjs";
|
|
3
3
|
import { SilgiError, isDefinedError, toSilgiError } from "./core/error.mjs";
|
|
4
|
-
import { extractRoutes } from "./core/router-utils.mjs";
|
|
5
4
|
import { compileProcedure, compileRouter, createContext } from "./compile.mjs";
|
|
6
5
|
import { initStorage, resetStorage, useStorage } from "./core/storage.mjs";
|
|
7
6
|
import { AsyncIteratorClass, mapAsyncIterator } from "./core/iterator.mjs";
|
|
@@ -12,4 +11,4 @@ import { lifecycleWrap } from "./lifecycle.mjs";
|
|
|
12
11
|
import { mapInput } from "./map-input.mjs";
|
|
13
12
|
import { isLazy, lazy, resolveLazy } from "./lazy.mjs";
|
|
14
13
|
import { generateOpenAPI, scalarHTML } from "./scalar.mjs";
|
|
15
|
-
export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, compileProcedure, compileRouter, createContext,
|
|
14
|
+
export { AsyncIteratorClass, SilgiError, ValidationError, callable, collectCronTasks, compileProcedure, compileRouter, createContext, generateOpenAPI, getEventMeta, getScheduledTasks, initStorage, isDefinedError, isLazy, lazy, lifecycleWrap, mapAsyncIterator, mapInput, resetStorage, resolveLazy, runTask, scalarHTML, setTaskAnalytics, silgi, startCronJobs, stopCronJobs, toSilgiError, type, useStorage, validateSchema, withEventMeta };
|
|
@@ -1,24 +1,4 @@
|
|
|
1
1
|
//#region src/integrations/better-auth/index.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Silgi + Better Auth tracing integration.
|
|
4
|
-
*
|
|
5
|
-
* Provides a Better Auth plugin factory that auto-traces all auth operations
|
|
6
|
-
* (sign-in, sign-up, OAuth, session management, etc.) into silgi analytics.
|
|
7
|
-
*
|
|
8
|
-
* The silgi request context is passed via `request.__silgiCtx`, set by
|
|
9
|
-
* the silgi auth handler before calling `auth.handler(request)`.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { tracing } from 'silgi/better-auth'
|
|
14
|
-
*
|
|
15
|
-
* const auth = betterAuth({
|
|
16
|
-
* plugins: [
|
|
17
|
-
* tracing(), // auto-traces all auth operations
|
|
18
|
-
* ],
|
|
19
|
-
* })
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
2
|
interface TracingConfig {
|
|
23
3
|
/** Capture request body as span input (default: true) */
|
|
24
4
|
captureInput?: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getCtx, runWithCtx } from "../../core/context-bridge.mjs";
|
|
2
2
|
//#region src/integrations/better-auth/index.ts
|
|
3
3
|
/**
|
|
4
4
|
* Silgi + Better Auth tracing integration.
|
|
@@ -20,7 +20,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
20
20
|
* })
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
const ctxStorage = new AsyncLocalStorage();
|
|
24
23
|
function matchOperation(path) {
|
|
25
24
|
const normalized = path.replace(/^\/+/, "");
|
|
26
25
|
if (normalized.endsWith("/sign-up/email") || normalized === "sign-up/email") return {
|
|
@@ -281,11 +280,11 @@ function instrumentBetterAuth(auth) {
|
|
|
281
280
|
* Run a function with silgi context available to instrumented Better Auth API calls.
|
|
282
281
|
*/
|
|
283
282
|
function withCtx(ctx, fn) {
|
|
284
|
-
return
|
|
283
|
+
return runWithCtx(ctx, fn);
|
|
285
284
|
}
|
|
286
285
|
function wrapApiMethod(originalFn, operation, method) {
|
|
287
286
|
return async function instrumented(...args) {
|
|
288
|
-
const reqTrace =
|
|
287
|
+
const reqTrace = getCtx()?.__analyticsTrace;
|
|
289
288
|
if (!reqTrace) return originalFn.apply(this, args);
|
|
290
289
|
const spanName = `auth.api.${operation}`;
|
|
291
290
|
const start = performance.now();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getCtx, runWithCtx } from "../../core/context-bridge.mjs";
|
|
2
2
|
//#region src/integrations/drizzle/index.ts
|
|
3
3
|
/**
|
|
4
4
|
* Silgi + Drizzle ORM tracing integration.
|
|
@@ -31,7 +31,6 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
31
31
|
* })
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
|
-
const ctxStorage = new AsyncLocalStorage();
|
|
35
34
|
const INSTRUMENTED = "__silgiDrizzleInstrumented";
|
|
36
35
|
const DEFAULT_DB_SYSTEM = "postgresql";
|
|
37
36
|
const DEFAULT_MAX_QUERY_LENGTH = 1e3;
|
|
@@ -59,7 +58,7 @@ function instrumentDrizzle(db, config) {
|
|
|
59
58
|
* All Drizzle queries inside `fn` will be recorded as trace spans.
|
|
60
59
|
*/
|
|
61
60
|
function withCtx(ctx, fn) {
|
|
62
|
-
return
|
|
61
|
+
return runWithCtx(ctx, fn);
|
|
63
62
|
}
|
|
64
63
|
function resolveConfig(config) {
|
|
65
64
|
return {
|
|
@@ -84,7 +83,7 @@ function patchSession(session, cfg, isTx) {
|
|
|
84
83
|
session.prepareQuery = function patchedPrepareQuery(...args) {
|
|
85
84
|
const prepared = originalPrepareQuery.apply(this, args);
|
|
86
85
|
if (!prepared || typeof prepared.execute !== "function") return prepared;
|
|
87
|
-
const reqTrace =
|
|
86
|
+
const reqTrace = getCtx()?.__analyticsTrace;
|
|
88
87
|
if (!reqTrace) return prepared;
|
|
89
88
|
const queryText = extractQueryText(args[0]) ?? prepared.rawQueryConfig?.text ?? prepared.queryConfig?.text ?? null;
|
|
90
89
|
const originalExecute = prepared.execute.bind(prepared);
|
|
@@ -98,7 +97,7 @@ function patchSession(session, cfg, isTx) {
|
|
|
98
97
|
if (typeof session.query === "function") {
|
|
99
98
|
const originalQuery = session.query.bind(session);
|
|
100
99
|
session.query = function patchedQuery(queryString, params) {
|
|
101
|
-
const reqTrace =
|
|
100
|
+
const reqTrace = getCtx()?.__analyticsTrace;
|
|
102
101
|
if (!reqTrace) return originalQuery.call(this, queryString, params);
|
|
103
102
|
return traceExecution(reqTrace, cfg, queryString ?? null, isTx, originalQuery, this, [queryString, params]);
|
|
104
103
|
};
|
|
@@ -127,7 +126,7 @@ function patchRawClient(client, cfg) {
|
|
|
127
126
|
if (!methodName) return false;
|
|
128
127
|
const originalMethod = client[methodName].bind(client);
|
|
129
128
|
client[methodName] = function patchedClientMethod(...args) {
|
|
130
|
-
const reqTrace =
|
|
129
|
+
const reqTrace = getCtx()?.__analyticsTrace;
|
|
131
130
|
if (!reqTrace) return originalMethod.apply(this, args);
|
|
132
131
|
return traceExecution(reqTrace, cfg, extractQueryText(args[0]) ?? null, false, originalMethod, this, args);
|
|
133
132
|
};
|
|
@@ -141,7 +140,7 @@ function patchSessionExecute(session, cfg) {
|
|
|
141
140
|
if (session[INSTRUMENTED]) return false;
|
|
142
141
|
const originalExecute = session.execute.bind(session);
|
|
143
142
|
session.execute = function patchedDeepExecute(...args) {
|
|
144
|
-
const reqTrace =
|
|
143
|
+
const reqTrace = getCtx()?.__analyticsTrace;
|
|
145
144
|
if (!reqTrace) return originalExecute.apply(this, args);
|
|
146
145
|
return traceExecution(reqTrace, cfg, extractQueryText(args[0]) ?? null, false, originalExecute, this, args);
|
|
147
146
|
};
|
|
@@ -11,7 +11,7 @@ import { FetchHandler } from "../core/handler.mjs";
|
|
|
11
11
|
* - HTTP-level request tracking with procedure grouping (batch support)
|
|
12
12
|
* - Unique request IDs via `x-request-id` response header
|
|
13
13
|
*
|
|
14
|
-
* Dashboard at /analytics, JSON API at /analytics/
|
|
14
|
+
* Dashboard at /api/analytics, JSON API at /api/analytics/stats, errors at /api/analytics/errors.
|
|
15
15
|
*/
|
|
16
16
|
interface TimeWindow {
|
|
17
17
|
time: number;
|
|
@@ -66,6 +66,7 @@ interface RequestEntry {
|
|
|
66
66
|
timestamp: number;
|
|
67
67
|
durationMs: number;
|
|
68
68
|
method: string;
|
|
69
|
+
url: string;
|
|
69
70
|
path: string;
|
|
70
71
|
ip: string;
|
|
71
72
|
headers: Record<string, string>;
|
|
@@ -93,12 +94,6 @@ interface AnalyticsOptions {
|
|
|
93
94
|
bufferSize?: number;
|
|
94
95
|
/** Time-series history in seconds (default: 120) */
|
|
95
96
|
historySeconds?: number;
|
|
96
|
-
/** Max error entries to keep (default: 100) */
|
|
97
|
-
maxErrors?: number;
|
|
98
|
-
/** Max recent request entries to keep (default: 200) */
|
|
99
|
-
maxRequests?: number;
|
|
100
|
-
/** Max task execution entries to keep (default: 200) */
|
|
101
|
-
maxTasks?: number;
|
|
102
97
|
/**
|
|
103
98
|
* Protect dashboard access.
|
|
104
99
|
* - `string` — secret token checked against `Authorization: Bearer <token>` header or `?token=` query param
|
|
@@ -108,6 +103,10 @@ interface AnalyticsOptions {
|
|
|
108
103
|
auth?: string | ((req: Request) => boolean | Promise<boolean>);
|
|
109
104
|
/** Interval in ms between storage flushes (default: 5000) */
|
|
110
105
|
flushInterval?: number;
|
|
106
|
+
/** Days to retain entries in storage (default: 30). Entries older than this are pruned on flush. */
|
|
107
|
+
retentionDays?: number;
|
|
108
|
+
/** Path prefixes to exclude from tracking. Can also be managed at runtime via the dashboard or API. */
|
|
109
|
+
ignorePaths?: string[];
|
|
111
110
|
}
|
|
112
111
|
interface ProcedureSnapshot {
|
|
113
112
|
count: number;
|
|
@@ -189,6 +188,13 @@ declare function trace<T>(ctx: Record<string, unknown>, name: string, fn: () =>
|
|
|
189
188
|
declare class AnalyticsCollector {
|
|
190
189
|
#private;
|
|
191
190
|
constructor(options?: AnalyticsOptions);
|
|
191
|
+
/** Check if a path is server-side ignored (from config). */
|
|
192
|
+
isIgnored(pathname: string): boolean;
|
|
193
|
+
/** Check if a path is hidden in the dashboard (from runtime API). */
|
|
194
|
+
isHidden(pathname: string): boolean;
|
|
195
|
+
addHiddenPath(path: string): void;
|
|
196
|
+
removeHiddenPath(path: string): void;
|
|
197
|
+
getHiddenPaths(): string[];
|
|
192
198
|
record(path: string, durationMs: number): void;
|
|
193
199
|
recordError(path: string, durationMs: number, errorMsg: string): void;
|
|
194
200
|
recordDetailedError(entry: Omit<ErrorEntry, 'id'>): void;
|
|
@@ -224,7 +230,7 @@ declare function sanitizeHeaders(headers: Headers): Record<string, string>;
|
|
|
224
230
|
/** Return auth-failure response for analytics routes. */
|
|
225
231
|
declare function analyticsAuthResponse(pathname: string): Response;
|
|
226
232
|
/** Serve analytics dashboard and API routes. */
|
|
227
|
-
declare function serveAnalyticsRoute(pathname: string, collector: AnalyticsCollector, dashboardHtml: string | undefined): Promise<Response>;
|
|
233
|
+
declare function serveAnalyticsRoute(pathname: string, request: Request, collector: AnalyticsCollector, dashboardHtml: string | undefined): Promise<Response>;
|
|
228
234
|
/**
|
|
229
235
|
* Wrap a fetch handler with analytics collection.
|
|
230
236
|
* Intercepts analytics dashboard routes and instruments every request.
|