@tellann/backend-sdk 0.1.0 → 0.3.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.
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tellannHapiPlugin = void 0;
4
+ const TELLANN_1 = require("../../core/TELLANN");
5
+ const express_1 = require("../express");
6
+ const requestContext_1 = require("../../core/requestContext");
7
+ function statusOf(request) {
8
+ const response = request.response;
9
+ if (!response)
10
+ return 0;
11
+ return (response.isBoom ? response.output?.statusCode : response.statusCode) ?? 0;
12
+ }
13
+ /**
14
+ * The hapi plugin object, registered with `await server.register(tellannHapiPlugin)`.
15
+ */
16
+ exports.tellannHapiPlugin = {
17
+ name: 'tellann',
18
+ version: '1.0.0',
19
+ register(server) {
20
+ server.ext('onRequest', (request, h) => {
21
+ request.app.tellann = {
22
+ ...(0, express_1.extractCorrelationContext)(request.headers ?? {}),
23
+ startedAt: Date.now(),
24
+ };
25
+ // hapi extensions do not wrap the handler, so the context is bound to
26
+ // this execution rather than to a callback, and kept on the request for
27
+ // the response extension to read back.
28
+ request.app.tellannContext = (0, requestContext_1.enterRequestContext)({
29
+ ...(0, express_1.extractCorrelationContext)(request.headers ?? {}),
30
+ method: request.method?.toUpperCase?.() ?? 'GET',
31
+ route: request.route?.path ?? request.path,
32
+ dataAccess: [],
33
+ });
34
+ return h.continue;
35
+ });
36
+ server.ext('onPreResponse', (request, h) => {
37
+ const correlation = request.app.tellann ?? {};
38
+ const startedAt = typeof correlation.startedAt === 'number' ? correlation.startedAt : Date.now();
39
+ const context = request.app.tellannContext;
40
+ void TELLANN_1.TELLANN.trackApi({
41
+ endpoint: request.path,
42
+ // The route table's pattern, so `/users/{id}` stays one endpoint.
43
+ route: request.route?.path ?? request.path,
44
+ method: request.method?.toUpperCase?.() ?? 'GET',
45
+ statusCode: statusOf(request),
46
+ durationMs: Date.now() - startedAt,
47
+ sessionId: correlation.sessionId,
48
+ runId: correlation.runId,
49
+ traceId: correlation.traceId,
50
+ framework: 'hapi',
51
+ models: (0, requestContext_1.summarizeDataAccess)(context?.dataAccess ?? []),
52
+ query: request.query,
53
+ requestBody: request.payload,
54
+ // A Boom error's `source` is the error payload hapi will serialize.
55
+ responseBody: request.response?.isBoom ? undefined : request.response?.source,
56
+ requestHeaders: request.headers,
57
+ responseHeaders: request.response?.headers,
58
+ });
59
+ void TELLANN_1.TELLANN.flushDataAccess(context);
60
+ if (request.response?.isBoom) {
61
+ void TELLANN_1.TELLANN.captureError({
62
+ error: request.response,
63
+ sessionId: correlation.sessionId,
64
+ runId: correlation.runId,
65
+ traceId: correlation.traceId,
66
+ eventType: 'SERVER_ERROR',
67
+ route: request.route?.path ?? request.path,
68
+ method: request.method?.toUpperCase?.() ?? 'GET',
69
+ statusCode: statusOf(request),
70
+ });
71
+ }
72
+ return h.continue;
73
+ });
74
+ },
75
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Koa integration.
3
+ *
4
+ * Koa's types are described structurally rather than imported, so installing
5
+ * this SDK never drags `koa` and `@types/koa` into a project that does not use
6
+ * them. The shape used here is the stable part of Koa's context contract.
7
+ */
8
+ export type TellannKoaContext = {
9
+ method: string;
10
+ path: string;
11
+ status: number;
12
+ /** Set by `koa-router`; the matched pattern rather than the concrete path. */
13
+ _matchedRoute?: string;
14
+ /** Present when a body parser is registered. */
15
+ body?: unknown;
16
+ query?: Record<string, unknown>;
17
+ request: {
18
+ headers: Record<string, any>;
19
+ body?: unknown;
20
+ };
21
+ response?: {
22
+ headers?: Record<string, any>;
23
+ };
24
+ state: Record<string, any>;
25
+ };
26
+ export type TellannKoaMiddleware = (context: TellannKoaContext, next: () => Promise<any>) => Promise<void>;
27
+ /**
28
+ * Track every request, and re-throw whatever the downstream middleware threw.
29
+ *
30
+ * The matched router pattern is preferred over `ctx.path`: reporting the
31
+ * concrete path would put identifiers from URLs into telemetry and would make
32
+ * every request to `/users/:id` a distinct endpoint.
33
+ */
34
+ export declare function tellannKoaMiddleware(): TellannKoaMiddleware;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tellannKoaMiddleware = tellannKoaMiddleware;
4
+ const TELLANN_1 = require("../../core/TELLANN");
5
+ const express_1 = require("../express");
6
+ const requestContext_1 = require("../../core/requestContext");
7
+ /**
8
+ * Track every request, and re-throw whatever the downstream middleware threw.
9
+ *
10
+ * The matched router pattern is preferred over `ctx.path`: reporting the
11
+ * concrete path would put identifiers from URLs into telemetry and would make
12
+ * every request to `/users/:id` a distinct endpoint.
13
+ */
14
+ function tellannKoaMiddleware() {
15
+ return async (context, next) => {
16
+ const start = Date.now();
17
+ const correlation = (0, express_1.extractCorrelationContext)(context.request?.headers ?? {});
18
+ context.state.tellann = correlation;
19
+ const tellannContext = {
20
+ ...correlation,
21
+ method: context.method,
22
+ route: context._matchedRoute ?? context.path,
23
+ dataAccess: [],
24
+ };
25
+ await (0, requestContext_1.runInRequestContext)(tellannContext, async () => {
26
+ try {
27
+ await next();
28
+ }
29
+ catch (error) {
30
+ await TELLANN_1.TELLANN.captureError({
31
+ error: error,
32
+ sessionId: correlation.sessionId,
33
+ runId: correlation.runId,
34
+ traceId: correlation.traceId,
35
+ eventType: 'SERVER_ERROR',
36
+ route: context._matchedRoute ?? context.path,
37
+ method: context.method,
38
+ statusCode: context.status,
39
+ });
40
+ throw error;
41
+ }
42
+ finally {
43
+ await TELLANN_1.TELLANN.trackApi({
44
+ endpoint: context.path,
45
+ // Read after `next`, by which point the router has matched.
46
+ route: context._matchedRoute ?? context.path,
47
+ method: context.method,
48
+ statusCode: context.status,
49
+ durationMs: Date.now() - start,
50
+ sessionId: correlation.sessionId,
51
+ runId: correlation.runId,
52
+ traceId: correlation.traceId,
53
+ framework: 'koa',
54
+ models: (0, requestContext_1.summarizeDataAccess)(tellannContext.dataAccess),
55
+ query: context.query,
56
+ requestBody: context.request?.body,
57
+ responseBody: context.body,
58
+ requestHeaders: context.request?.headers,
59
+ responseHeaders: context.response?.headers,
60
+ });
61
+ await TELLANN_1.TELLANN.flushDataAccess(tellannContext);
62
+ }
63
+ });
64
+ };
65
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Prisma integration.
3
+ *
4
+ * Prisma's types are described structurally rather than imported, so this SDK
5
+ * never pulls `@prisma/client` into a project that does not use it. Both of
6
+ * Prisma's interception points are supported because which one a project can
7
+ * use depends on its version: `$extends` on 4.16 and later, `$use` before it.
8
+ */
9
+ export type TellannPrismaOperationArgs = {
10
+ model?: string | null;
11
+ operation: string;
12
+ args: unknown;
13
+ query: (args: unknown) => Promise<unknown>;
14
+ };
15
+ export type TellannPrismaMiddlewareParams = {
16
+ model?: string | null;
17
+ action: string;
18
+ args?: unknown;
19
+ };
20
+ /**
21
+ * A Prisma client extension that reports every query as a data-access event.
22
+ *
23
+ * Usage:
24
+ * const prisma = new PrismaClient().$extends(tellannPrismaExtension());
25
+ *
26
+ * The extension reports the model and the operation, never the arguments: a
27
+ * `where` clause routinely contains the identifiers a QA run is required not
28
+ * to keep in the clear, and the request's own captured payload already says
29
+ * what was asked for.
30
+ */
31
+ export declare function tellannPrismaExtension(): {
32
+ name: string;
33
+ query: {
34
+ $allModels: {
35
+ $allOperations({ model, operation, args, query }: TellannPrismaOperationArgs): Promise<unknown>;
36
+ };
37
+ };
38
+ };
39
+ /**
40
+ * The same reporting for Prisma clients older than 4.16, which have `$use`
41
+ * rather than `$extends`.
42
+ *
43
+ * Usage:
44
+ * prisma.$use(tellannPrismaMiddleware());
45
+ */
46
+ export declare function tellannPrismaMiddleware(): (params: TellannPrismaMiddlewareParams, next: (params: TellannPrismaMiddlewareParams) => Promise<unknown>) => Promise<unknown>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.tellannPrismaExtension = tellannPrismaExtension;
4
+ exports.tellannPrismaMiddleware = tellannPrismaMiddleware;
5
+ const TELLANN_1 = require("../../core/TELLANN");
6
+ const trackDataAccess_1 = require("../../core/trackDataAccess");
7
+ function recordCount(result) {
8
+ if (Array.isArray(result))
9
+ return result.length;
10
+ if (result && typeof result === 'object') {
11
+ const count = result.count;
12
+ if (typeof count === 'number')
13
+ return count;
14
+ return 1;
15
+ }
16
+ return result === null || result === undefined ? 0 : 1;
17
+ }
18
+ async function report(model, operation, startedAt, result) {
19
+ if (!model)
20
+ return;
21
+ await TELLANN_1.TELLANN.trackDataAccess({
22
+ model,
23
+ operation,
24
+ records: recordCount(result),
25
+ durationMs: Date.now() - startedAt,
26
+ mutation: (0, trackDataAccess_1.isMutationOperation)(operation),
27
+ }).catch(() => undefined);
28
+ }
29
+ /**
30
+ * A Prisma client extension that reports every query as a data-access event.
31
+ *
32
+ * Usage:
33
+ * const prisma = new PrismaClient().$extends(tellannPrismaExtension());
34
+ *
35
+ * The extension reports the model and the operation, never the arguments: a
36
+ * `where` clause routinely contains the identifiers a QA run is required not
37
+ * to keep in the clear, and the request's own captured payload already says
38
+ * what was asked for.
39
+ */
40
+ function tellannPrismaExtension() {
41
+ return {
42
+ name: 'tellann',
43
+ query: {
44
+ $allModels: {
45
+ async $allOperations({ model, operation, args, query }) {
46
+ const startedAt = Date.now();
47
+ const result = await query(args);
48
+ void report(model, operation, startedAt, result);
49
+ return result;
50
+ },
51
+ },
52
+ },
53
+ };
54
+ }
55
+ /**
56
+ * The same reporting for Prisma clients older than 4.16, which have `$use`
57
+ * rather than `$extends`.
58
+ *
59
+ * Usage:
60
+ * prisma.$use(tellannPrismaMiddleware());
61
+ */
62
+ function tellannPrismaMiddleware() {
63
+ return async (params, next) => {
64
+ const startedAt = Date.now();
65
+ const result = await next(params);
66
+ void report(params.model, params.action, startedAt, result);
67
+ return result;
68
+ };
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tellann/backend-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Tellann server telemetry and QA-run correlation SDK",
5
5
  "license": "UNLICENSED",
6
6
  "main": "dist/index.js",
@@ -1,39 +0,0 @@
1
- import { TrackApiOptions } from './trackApi';
2
- import { CaptureErrorOptions } from './captureError';
3
- import { TrackStateOptions } from './trackState';
4
- import { BackendWorkflowTracker } from './workflowTracker';
5
- import type { EventType } from '../event-types';
6
- export interface SotsBackendConfig {
7
- endpoint: string;
8
- tenantId?: string;
9
- applicationId: string;
10
- apiKey?: string;
11
- environmentId?: string;
12
- runId?: string;
13
- sessionId?: string;
14
- traceId?: string;
15
- agentVersion?: string;
16
- instrumentationManifestVersion?: string;
17
- }
18
- export declare class SOTSBackend {
19
- private config;
20
- private workflowTracker;
21
- initialize(config: SotsBackendConfig): void;
22
- getConfig(): SotsBackendConfig | null;
23
- isInitialized(): boolean;
24
- trackApi(options: TrackApiOptions): Promise<void>;
25
- captureError(options: CaptureErrorOptions): Promise<void>;
26
- trackState(options: TrackStateOptions): Promise<void>;
27
- trackEvent(eventType: EventType, metadata?: Record<string, any>, sessionId?: string): Promise<void>;
28
- verifyInstallation(sessionId?: string): Promise<void>;
29
- startWorkflow(workflowName: string, sessionId?: string): string;
30
- completeWorkflow(workflowId: string, sessionId?: string): Promise<void>;
31
- failWorkflow(workflowId: string, reason?: string, sessionId?: string): Promise<void>;
32
- abandonWorkflow(workflowId: string): void;
33
- cancelWorkflow(workflowId: string, reason?: string, sessionId?: string): Promise<void>;
34
- captureMessage(message: string, severity?: string, sessionId?: string): Promise<void>;
35
- private sendEvent;
36
- teardown(): void;
37
- }
38
- export declare const SOTS: SOTSBackend;
39
- export { BackendWorkflowTracker };
package/dist/core/SOTS.js DELETED
@@ -1,158 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BackendWorkflowTracker = exports.SOTS = exports.SOTSBackend = void 0;
4
- const trackApi_1 = require("./trackApi");
5
- const captureError_1 = require("./captureError");
6
- const trackState_1 = require("./trackState");
7
- const workflowTracker_1 = require("./workflowTracker");
8
- Object.defineProperty(exports, "BackendWorkflowTracker", { enumerable: true, get: function () { return workflowTracker_1.BackendWorkflowTracker; } });
9
- const uuid_1 = require("uuid");
10
- class SOTSBackend {
11
- config = null;
12
- workflowTracker = new workflowTracker_1.BackendWorkflowTracker();
13
- initialize(config) {
14
- this.config = config;
15
- console.log('[Tellann Backend] Initialized');
16
- }
17
- getConfig() {
18
- return this.config;
19
- }
20
- isInitialized() {
21
- return this.config !== null;
22
- }
23
- async trackApi(options) {
24
- if (!this.config)
25
- return;
26
- await (0, trackApi_1.trackApiEvent)(this.config, options);
27
- }
28
- async captureError(options) {
29
- if (!this.config)
30
- return;
31
- await (0, captureError_1.captureErrorEvent)(this.config, options);
32
- }
33
- async trackState(options) {
34
- if (!this.config)
35
- return;
36
- await (0, trackState_1.trackStateEvent)(this.config, options);
37
- }
38
- async trackEvent(eventType, metadata = {}, sessionId) {
39
- await this.sendEvent(eventType, sessionId, metadata);
40
- }
41
- async verifyInstallation(sessionId) {
42
- await this.trackEvent('TELLANN_INITIALIZED', {
43
- source: 'manual_verification',
44
- verificationKind: 'BOOTSTRAP_INITIALIZED',
45
- }, sessionId);
46
- }
47
- startWorkflow(workflowName, sessionId) {
48
- const id = this.workflowTracker.start(workflowName);
49
- this.sendEvent('WORKFLOW_STARTED', sessionId, {
50
- workflowId: id,
51
- workflowName,
52
- });
53
- return id;
54
- }
55
- async completeWorkflow(workflowId, sessionId) {
56
- const result = this.workflowTracker.complete(workflowId);
57
- if (result) {
58
- await this.sendEvent('WORKFLOW_COMPLETED', sessionId, {
59
- workflowId,
60
- workflowName: result.name,
61
- durationMs: result.durationMs,
62
- });
63
- }
64
- }
65
- async failWorkflow(workflowId, reason, sessionId) {
66
- const result = this.workflowTracker.fail(workflowId);
67
- if (result) {
68
- await this.sendEvent('WORKFLOW_FAILED', sessionId, {
69
- workflowId,
70
- workflowName: result.name,
71
- durationMs: result.durationMs,
72
- reason: reason || 'Unknown error',
73
- });
74
- }
75
- }
76
- abandonWorkflow(workflowId) {
77
- this.workflowTracker.abandon(workflowId);
78
- }
79
- async cancelWorkflow(workflowId, reason, sessionId) {
80
- const result = this.workflowTracker.fail(workflowId);
81
- if (result) {
82
- await this.sendEvent('WORKFLOW_CANCELLED', sessionId, {
83
- workflowId,
84
- workflowName: result.name,
85
- durationMs: result.durationMs,
86
- reason: reason ?? 'Cancelled',
87
- });
88
- }
89
- }
90
- async captureMessage(message, severity, sessionId) {
91
- await this.sendEvent('SERVER_ERROR', sessionId, {
92
- message,
93
- severity: severity || 'error',
94
- });
95
- }
96
- async sendEvent(eventType, sessionId, metadata) {
97
- if (!this.config)
98
- return;
99
- const event = {
100
- eventId: (0, uuid_1.v4)(),
101
- sessionId: sessionId ?? this.config.sessionId ?? (0, uuid_1.v4)(),
102
- tenantId: this.config.tenantId ?? 'unknown',
103
- applicationId: this.config.applicationId,
104
- environmentId: this.config.environmentId ?? null,
105
- runId: this.config.runId ?? null,
106
- traceId: this.config.traceId ?? null,
107
- agentVersion: this.config.agentVersion ?? null,
108
- instrumentationManifestVersion: this.config.instrumentationManifestVersion ?? null,
109
- source: 'backend-sdk',
110
- eventVersion: '1.0',
111
- eventType: eventType,
112
- timestamp: new Date().toISOString(),
113
- metadata,
114
- };
115
- // Enforce size limit
116
- try {
117
- const eventJson = JSON.stringify(event);
118
- const eventSize = Buffer.byteLength(eventJson, 'utf8');
119
- if (eventSize > 32 * 1024) {
120
- console.error(`[Tellann Backend] Event of type "${eventType}" discarded. Size (${eventSize} bytes) exceeds limit of 32 KB.`);
121
- return;
122
- }
123
- }
124
- catch {
125
- return;
126
- }
127
- try {
128
- const headers = { 'Content-Type': 'application/json' };
129
- if (this.config.apiKey) {
130
- headers.Authorization = `Bearer ${this.config.apiKey}`;
131
- }
132
- if (this.config.environmentId) {
133
- headers['x-sots-environment-id'] = this.config.environmentId;
134
- }
135
- if (this.config.runId)
136
- headers['x-tellann-run-id'] = this.config.runId;
137
- if (sessionId ?? this.config.sessionId)
138
- headers['x-tellann-session-id'] = sessionId ?? this.config.sessionId;
139
- if (this.config.traceId)
140
- headers['x-tellann-trace-id'] = this.config.traceId;
141
- await fetch(`${this.config.endpoint}/v1/events`, {
142
- method: 'POST',
143
- headers,
144
- body: JSON.stringify(event),
145
- });
146
- }
147
- catch {
148
- // Swallowed
149
- }
150
- }
151
- // Allow teardown to clean up intervals/tracker memory
152
- teardown() {
153
- this.workflowTracker.destroy();
154
- this.config = null;
155
- }
156
- }
157
- exports.SOTSBackend = SOTSBackend;
158
- exports.SOTS = new SOTSBackend();