@tellann/frontend-sdk 0.1.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 ADDED
@@ -0,0 +1,92 @@
1
+ # @tellann/frontend-sdk
2
+
3
+ Browser telemetry and QA-run correlation SDK for Tellann. Captures page views, clicks,
4
+ form submissions, route changes, errors, workflow lifecycle events, and custom business
5
+ events, then batches them to a Tellann collector endpoint.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @tellann/frontend-sdk
11
+ # or
12
+ pnpm add @tellann/frontend-sdk
13
+ ```
14
+
15
+ This is an ES module and targets modern browsers (`fetch`, `navigator.sendBeacon`, `Blob`).
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { TELLANN } from '@tellann/frontend-sdk';
21
+
22
+ TELLANN.initialize({
23
+ endpoint: 'https://collector.example.com',
24
+ applicationId: 'my-web-app',
25
+ environmentId: 'production',
26
+ apiKey: '<publishable-key>', // optional
27
+ autoTrackClicks: true, // default true
28
+ autoTrackForms: true, // default true
29
+ autoTrackRoutes: true, // default true
30
+ errorTracking: true, // default true
31
+ debug: false,
32
+ });
33
+ ```
34
+
35
+ `initialize` starts a session (emitting a `PAGE_VIEW`), wires up auto-tracking, and
36
+ begins a flush interval (`flushIntervalMs`, default 5000). Events are also flushed
37
+ immediately when the buffer reaches `maxBufferSize` (default 200).
38
+
39
+ ## Configuration
40
+
41
+ | Option | Type | Default | Notes |
42
+ | --- | --- | --- | --- |
43
+ | `endpoint` | `string` | – | Collector base URL. Events POST to `${endpoint}/v1/events/batch`. |
44
+ | `applicationId` | `string` | – | Identifies the app sending events. |
45
+ | `tenantId` | `string` | `'unknown'` | Optional tenant identifier. |
46
+ | `environmentId` | `string` | `null` | Sent as `x-tellann-environment-id`. |
47
+ | `apiKey` | `string` | – | Sent as `Authorization: Bearer`. Disables `sendBeacon` fallback. |
48
+ | `runId` / `sessionId` / `traceId` | `string` | generated | QA-run / trace correlation IDs. |
49
+ | `agentVersion` / `instrumentationManifestVersion` | `string` | `null` | Correlation metadata. |
50
+ | `autoTrackClicks` / `autoTrackForms` / `autoTrackRoutes` | `boolean` | `true` | DOM auto-instrumentation. |
51
+ | `errorTracking` | `boolean` | `true` | Capture uncaught errors and rejections. |
52
+ | `flushIntervalMs` | `number` | `5000` | Batch flush cadence. |
53
+ | `maxBufferSize` | `number` | `200` | Force a flush at this many buffered events. |
54
+ | `debug` | `boolean` | `false` | Verbose console logging. |
55
+
56
+ ## Manual tracking API
57
+
58
+ ```ts
59
+ TELLANN.trackEvent('PAGE_VIEW', { url: location.href });
60
+ TELLANN.trackBusinessEvent({ type: 'checkout_completed', payload: { total: 42 } });
61
+
62
+ TELLANN.trackState('cart_open', 'BUSINESS');
63
+ TELLANN.trackTransition('cart_open', 'checkout', 'NAVIGATE');
64
+
65
+ const wf = TELLANN.startWorkflow('signup');
66
+ TELLANN.completeWorkflow(wf); // or failWorkflow(wf, reason) / cancelWorkflow(wf)
67
+
68
+ TELLANN.captureException(err, { route: '/checkout' });
69
+ TELLANN.captureMessage('payment provider slow', 'warning');
70
+ TELLANN.identifyUser('user_123', { plan: 'pro' });
71
+
72
+ await TELLANN.verifyInstallation(); // emits TELLANN_INITIALIZED and flushes
73
+ ```
74
+
75
+ Call `TELLANN.teardown()` on unmount / page teardown to clear the flush interval,
76
+ detach auto-tracking listeners, and flush remaining events.
77
+
78
+ ### Payload limits
79
+
80
+ - Standard event: 32 KB (oversized events are dropped with a console error).
81
+ - Replay events (`eventType` containing `REPLAY`): 128 KB.
82
+ - Batch payload: 5 MB (oversized batches are dropped).
83
+
84
+ Metadata is sanitized before send (privacy-by-default).
85
+
86
+ ## TypeScript
87
+
88
+ Types ship with the package. `TellannConfig`, `EventType`, and `TellannEvent` are exported.
89
+
90
+ ## License
91
+
92
+ `UNLICENSED` — see the repository for terms.
@@ -0,0 +1,12 @@
1
+ import type { EventType } from './event-types.js';
2
+ interface AutoTrackConfig {
3
+ autoTrackClicks?: boolean;
4
+ autoTrackForms?: boolean;
5
+ autoTrackRoutes?: boolean;
6
+ errorTracking?: boolean;
7
+ }
8
+ export declare function sanitizeMetadata(metadata: Record<string, any>): Record<string, any>;
9
+ export declare function setupAutoTrack(sdk: {
10
+ trackEvent: (type: EventType, metadata?: Record<string, any>) => void;
11
+ }, config: AutoTrackConfig): () => void;
12
+ export {};
@@ -0,0 +1,200 @@
1
+ // Utility to check if element or its parents should be ignored
2
+ function shouldIgnore(element) {
3
+ let curr = element;
4
+ while (curr) {
5
+ if (curr.hasAttribute && curr.hasAttribute('data-tellann-ignore')) {
6
+ return true;
7
+ }
8
+ // Ignore password inputs completely
9
+ if (curr.tagName === 'INPUT' && curr.type === 'password') {
10
+ return true;
11
+ }
12
+ curr = curr.parentElement;
13
+ }
14
+ return false;
15
+ }
16
+ // CSS Selector generator
17
+ function getCssSelector(el) {
18
+ if (el.id)
19
+ return `#${el.id}`;
20
+ let path = [];
21
+ let curr = el;
22
+ while (curr && curr.nodeType === Node.ELEMENT_NODE) {
23
+ let selector = curr.nodeName.toLowerCase();
24
+ if (curr.className) {
25
+ selector += `.${curr.className.trim().split(/\s+/).join('.')}`;
26
+ }
27
+ path.unshift(selector);
28
+ curr = curr.parentElement;
29
+ }
30
+ return path.join(' > ');
31
+ }
32
+ // Utility to recursively sanitize sensitive keys in metadata
33
+ export function sanitizeMetadata(metadata) {
34
+ const sensitiveKeys = [
35
+ 'password',
36
+ 'credit_card',
37
+ 'cvv',
38
+ 'token',
39
+ 'secret',
40
+ 'private_key',
41
+ 'access_token',
42
+ 'authorization'
43
+ ];
44
+ const sanitize = (val) => {
45
+ if (val === null || val === undefined)
46
+ return val;
47
+ if (Array.isArray(val)) {
48
+ return val.map(sanitize);
49
+ }
50
+ if (typeof val === 'object') {
51
+ const result = {};
52
+ for (const key of Object.keys(val)) {
53
+ const lowerKey = key.toLowerCase();
54
+ if (sensitiveKeys.some(sk => lowerKey.includes(sk))) {
55
+ result[key] = '[REDACTED]';
56
+ }
57
+ else {
58
+ result[key] = sanitize(val[key]);
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ return val;
64
+ };
65
+ return sanitize(metadata);
66
+ }
67
+ export function setupAutoTrack(sdk, config) {
68
+ const cleanups = [];
69
+ // 1. Clicks (Buttons & Links)
70
+ if (config.autoTrackClicks !== false) {
71
+ const clickHandler = (e) => {
72
+ const target = e.target;
73
+ if (!target)
74
+ return;
75
+ // Find closest button or link
76
+ const interactiveEl = target.closest('a, button, [role="button"]');
77
+ if (!interactiveEl || shouldIgnore(interactiveEl))
78
+ return;
79
+ const isLink = interactiveEl.tagName === 'A';
80
+ const eventType = isLink ? 'LINK_CLICK' : 'BUTTON_CLICK';
81
+ const text = interactiveEl.innerText?.trim().slice(0, 100) || '';
82
+ const elementId = interactiveEl.id || '';
83
+ const selector = getCssSelector(interactiveEl);
84
+ const metadata = {
85
+ elementId,
86
+ text,
87
+ selector,
88
+ };
89
+ if (isLink) {
90
+ metadata.href = interactiveEl.getAttribute('href') || '';
91
+ }
92
+ sdk.trackEvent(eventType, metadata);
93
+ };
94
+ document.addEventListener('click', clickHandler, true);
95
+ cleanups.push(() => document.removeEventListener('click', clickHandler, true));
96
+ }
97
+ // 2. Form Submissions
98
+ if (config.autoTrackForms !== false) {
99
+ const submitHandler = (e) => {
100
+ const form = e.target;
101
+ if (!form || shouldIgnore(form))
102
+ return;
103
+ const formId = form.id || '';
104
+ const formAction = form.getAttribute('action') || '';
105
+ // Capture only names of fields, never values
106
+ const fields = [];
107
+ const elements = form.elements;
108
+ for (let i = 0; i < elements.length; i++) {
109
+ const item = elements[i];
110
+ if (item.name) {
111
+ fields.push(item.name);
112
+ }
113
+ else if (item.id) {
114
+ fields.push(item.id);
115
+ }
116
+ }
117
+ sdk.trackEvent('FORM_SUBMITTED', {
118
+ formId,
119
+ formAction,
120
+ fields,
121
+ });
122
+ };
123
+ document.addEventListener('submit', submitHandler, true);
124
+ cleanups.push(() => document.removeEventListener('submit', submitHandler, true));
125
+ }
126
+ // 3. SPA Route Changes
127
+ if (config.autoTrackRoutes !== false) {
128
+ let lastUrl = window.location.href;
129
+ const handleRouteChange = () => {
130
+ const currentUrl = window.location.href;
131
+ if (currentUrl === lastUrl)
132
+ return;
133
+ const from = lastUrl;
134
+ const to = currentUrl;
135
+ lastUrl = currentUrl;
136
+ sdk.trackEvent('ROUTE_CHANGE', { from, to });
137
+ // Emit settled PAGE_VIEW
138
+ setTimeout(() => {
139
+ sdk.trackEvent('PAGE_VIEW', {
140
+ url: window.location.href,
141
+ title: document.title,
142
+ referrer: document.referrer,
143
+ });
144
+ }, 50);
145
+ };
146
+ // Patch history pushState and replaceState
147
+ const origPushState = history.pushState;
148
+ const origReplaceState = history.replaceState;
149
+ history.pushState = function (...args) {
150
+ origPushState.apply(this, args);
151
+ handleRouteChange();
152
+ };
153
+ history.replaceState = function (...args) {
154
+ origReplaceState.apply(this, args);
155
+ handleRouteChange();
156
+ };
157
+ window.addEventListener('popstate', handleRouteChange);
158
+ window.addEventListener('hashchange', handleRouteChange);
159
+ cleanups.push(() => {
160
+ history.pushState = origPushState;
161
+ history.replaceState = origReplaceState;
162
+ window.removeEventListener('popstate', handleRouteChange);
163
+ window.removeEventListener('hashchange', handleRouteChange);
164
+ });
165
+ }
166
+ // 4. Unhandled Errors and Promise Rejections
167
+ if (config.errorTracking !== false) {
168
+ const errorHandler = (e) => {
169
+ // Ignore errors that don't look like actual exceptions or are cross-origin script issues
170
+ const message = e.message || 'Unknown window error';
171
+ const stack = e.error instanceof Error ? e.error.stack : null;
172
+ const name = e.error instanceof Error ? e.error.name : 'Error';
173
+ sdk.trackEvent('UNHANDLED_EXCEPTION', {
174
+ message,
175
+ stack,
176
+ name,
177
+ });
178
+ };
179
+ const rejectionHandler = (e) => {
180
+ const reason = e.reason;
181
+ const message = reason instanceof Error ? reason.message : String(reason);
182
+ const stack = reason instanceof Error ? reason.stack : null;
183
+ const name = reason instanceof Error ? reason.name : 'UnhandledRejection';
184
+ sdk.trackEvent('UNHANDLED_EXCEPTION', {
185
+ message,
186
+ stack,
187
+ name,
188
+ });
189
+ };
190
+ window.addEventListener('error', errorHandler);
191
+ window.addEventListener('unhandledrejection', rejectionHandler);
192
+ cleanups.push(() => {
193
+ window.removeEventListener('error', errorHandler);
194
+ window.removeEventListener('unhandledrejection', rejectionHandler);
195
+ });
196
+ }
197
+ return () => {
198
+ cleanups.forEach(cleanup => cleanup());
199
+ };
200
+ }
@@ -0,0 +1,17 @@
1
+ export type EventType = 'PAGE_VIEW' | 'ROUTE_CHANGE' | 'BUTTON_CLICK' | 'LINK_CLICK' | 'FORM_SUBMIT' | 'FORM_SUBMITTED' | 'API_REQUEST' | 'ERROR_EVENT' | 'ERROR_OCCURRED' | 'UNHANDLED_EXCEPTION' | 'SERVER_ERROR' | 'CLIENT_ERROR' | 'BUSINESS_EVENT' | 'STATE_ENTERED' | 'STATE_TRANSITION' | 'FLOW_INITIAL_STATE' | 'FLOW_STATE_REACHED' | 'FLOW_TRANSITION' | 'FLOW_TERMINAL_STATE' | 'WORKFLOW_STARTED' | 'WORKFLOW_COMPLETED' | 'WORKFLOW_FAILED' | 'WORKFLOW_CANCELLED' | 'TELLANN_ONBOARDING_TEST' | 'TELLANN_INITIALIZED' | 'QA_RUN_STARTED' | 'QA_RUN_COMPLETED' | 'QA_RUN_FAILED' | 'BROWSER_PAGE_LOADED' | 'BROWSER_CONSOLE_ERROR' | 'BROWSER_NETWORK_FAILED' | 'VISUAL_ASSERTION_FAILED' | 'ACCESSIBILITY_FINDING' | 'INSTRUMENTATION_VERIFIED' | 'REPOSITORY_SNAPSHOT_CREATED' | 'EXPECTED_FLOW_VERSION_SELECTED';
2
+ export interface TellannEvent {
3
+ eventId: string;
4
+ sessionId: string;
5
+ tenantId: string;
6
+ applicationId: string;
7
+ environmentId?: string | null;
8
+ runId?: string | null;
9
+ traceId?: string | null;
10
+ agentVersion?: string | null;
11
+ instrumentationManifestVersion?: string | null;
12
+ source: string;
13
+ eventVersion: string;
14
+ eventType: EventType;
15
+ timestamp: string;
16
+ metadata: Record<string, any>;
17
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ import type { EventType } from './event-types';
2
+ export type { EventType, TellannEvent } from './event-types';
3
+ export interface TellannConfig {
4
+ endpoint: string;
5
+ tenantId?: string;
6
+ applicationId: string;
7
+ apiKey?: string;
8
+ environmentId?: string;
9
+ autoTrackClicks?: boolean;
10
+ autoTrackForms?: boolean;
11
+ autoTrackRoutes?: boolean;
12
+ errorTracking?: boolean;
13
+ debug?: boolean;
14
+ flushIntervalMs?: number;
15
+ maxBufferSize?: number;
16
+ runId?: string;
17
+ sessionId?: string;
18
+ traceId?: string;
19
+ agentVersion?: string;
20
+ instrumentationManifestVersion?: string;
21
+ }
22
+ declare class TellannFrontendSDK {
23
+ private config;
24
+ private sessionId;
25
+ private eventBuffer;
26
+ private flushInterval;
27
+ private workflowTracker;
28
+ private teardownAutoTrack;
29
+ initialize(config: TellannConfig): void;
30
+ startSession(): void;
31
+ endSession(): void;
32
+ teardown(): void;
33
+ trackEvent(eventType: EventType, metadata?: Record<string, any>): void;
34
+ verifyInstallation(): Promise<void>;
35
+ trackBusinessEvent(config: {
36
+ type: string;
37
+ payload?: Record<string, any>;
38
+ }): void;
39
+ trackState(stateName: string, category?: string): void;
40
+ trackTransition(fromState: string, toState: string, action?: string): void;
41
+ startWorkflow(workflowName: string): string;
42
+ completeWorkflow(workflowId: string): void;
43
+ failWorkflow(workflowId: string, reason?: string): void;
44
+ abandonWorkflow(workflowId: string): void;
45
+ cancelWorkflow(workflowId: string, reason?: string): void;
46
+ captureException(error: Error | unknown, context?: Record<string, any>): void;
47
+ captureMessage(message: string, severity?: 'info' | 'warning' | 'error'): void;
48
+ identifyUser(userId: string, traits?: Record<string, any>): void;
49
+ private startFlushInterval;
50
+ private flush;
51
+ }
52
+ export declare const TELLANN: TellannFrontendSDK;
53
+ export { TellannFrontendSDK };
package/dist/index.js ADDED
@@ -0,0 +1,266 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { WorkflowTracker } from './workflow-tracker.js';
3
+ import { setupAutoTrack, sanitizeMetadata } from './auto-track.js';
4
+ const MAX_EVENT_SIZE_BYTES = 32 * 1024; // 32 KB limit for standard events
5
+ const MAX_REPLAY_SIZE_BYTES = 128 * 1024; // 128 KB limit for replay events (e.g. if eventType is a replay event)
6
+ class TellannFrontendSDK {
7
+ config = null;
8
+ sessionId = null;
9
+ eventBuffer = [];
10
+ flushInterval = null;
11
+ workflowTracker = new WorkflowTracker();
12
+ teardownAutoTrack = null;
13
+ initialize(config) {
14
+ this.config = {
15
+ autoTrackClicks: true,
16
+ autoTrackForms: true,
17
+ autoTrackRoutes: true,
18
+ errorTracking: true,
19
+ debug: false,
20
+ flushIntervalMs: 5000,
21
+ maxBufferSize: 200,
22
+ ...config
23
+ };
24
+ this.startSession();
25
+ this.startFlushInterval();
26
+ // Set up auto-tracking
27
+ this.teardownAutoTrack = setupAutoTrack(this, {
28
+ autoTrackClicks: this.config.autoTrackClicks,
29
+ autoTrackForms: this.config.autoTrackForms,
30
+ autoTrackRoutes: this.config.autoTrackRoutes,
31
+ errorTracking: this.config.errorTracking
32
+ });
33
+ if (this.config.debug) {
34
+ console.log('[Tellann] Initialized and auto-tracking started', this.config);
35
+ }
36
+ }
37
+ startSession() {
38
+ this.sessionId = this.config?.sessionId ?? uuidv4();
39
+ this.trackEvent('PAGE_VIEW', {
40
+ url: window.location.href,
41
+ title: document.title,
42
+ referrer: document.referrer,
43
+ });
44
+ }
45
+ endSession() {
46
+ this.sessionId = null;
47
+ this.flush();
48
+ }
49
+ teardown() {
50
+ if (this.flushInterval) {
51
+ clearInterval(this.flushInterval);
52
+ this.flushInterval = null;
53
+ }
54
+ if (this.teardownAutoTrack) {
55
+ this.teardownAutoTrack();
56
+ this.teardownAutoTrack = null;
57
+ }
58
+ this.endSession();
59
+ }
60
+ trackEvent(eventType, metadata = {}) {
61
+ if (!this.config || !this.sessionId) {
62
+ if (this.config?.debug) {
63
+ console.warn('[Tellann] SDK not initialized or session not started');
64
+ }
65
+ return;
66
+ }
67
+ // Apply privacy-by-default metadata sanitization
68
+ const sanitizedMetadata = sanitizeMetadata(metadata);
69
+ const event = {
70
+ eventId: uuidv4(),
71
+ sessionId: this.sessionId,
72
+ tenantId: this.config.tenantId ?? 'unknown',
73
+ applicationId: this.config.applicationId,
74
+ environmentId: this.config.environmentId ?? null,
75
+ runId: this.config.runId ?? null,
76
+ traceId: this.config.traceId ?? null,
77
+ agentVersion: this.config.agentVersion ?? null,
78
+ instrumentationManifestVersion: this.config.instrumentationManifestVersion ?? null,
79
+ source: 'frontend-sdk',
80
+ eventVersion: '1.0',
81
+ eventType,
82
+ timestamp: new Date().toISOString(),
83
+ metadata: sanitizedMetadata,
84
+ };
85
+ // Payload Size Enforcement
86
+ try {
87
+ const eventJson = JSON.stringify(event);
88
+ const eventSize = typeof Blob !== 'undefined'
89
+ ? new Blob([eventJson]).size
90
+ : eventJson.length;
91
+ const limit = eventType.includes('REPLAY') ? MAX_REPLAY_SIZE_BYTES : MAX_EVENT_SIZE_BYTES;
92
+ if (eventSize > limit) {
93
+ console.error(`[Tellann] Event of type "${eventType}" discarded. Size (${eventSize} bytes) exceeds limit of ${limit} bytes.`);
94
+ return;
95
+ }
96
+ }
97
+ catch (err) {
98
+ console.error('[Tellann] Failed to compute size of event, discarding', err);
99
+ return;
100
+ }
101
+ this.eventBuffer.push(event);
102
+ // If max buffer size reached, flush immediately
103
+ const maxBuffer = this.config.maxBufferSize ?? 200;
104
+ if (this.eventBuffer.length >= maxBuffer) {
105
+ this.flush();
106
+ }
107
+ }
108
+ async verifyInstallation() {
109
+ this.trackEvent('TELLANN_INITIALIZED', {
110
+ source: 'manual_verification',
111
+ verificationKind: 'BOOTSTRAP_INITIALIZED',
112
+ instrumentationManifestVersion: this.config?.instrumentationManifestVersion ?? null,
113
+ agentVersion: this.config?.agentVersion ?? null,
114
+ });
115
+ await this.flush();
116
+ }
117
+ trackBusinessEvent(config) {
118
+ this.trackEvent('BUSINESS_EVENT', {
119
+ businessEventType: config.type,
120
+ ...(config.payload || {})
121
+ });
122
+ }
123
+ // Missing Frontend SDK methods
124
+ trackState(stateName, category) {
125
+ this.trackEvent('STATE_ENTERED', {
126
+ stateName,
127
+ category: category || 'BUSINESS',
128
+ });
129
+ }
130
+ trackTransition(fromState, toState, action) {
131
+ this.trackEvent('STATE_TRANSITION', {
132
+ fromState,
133
+ toState,
134
+ action: action || 'NAVIGATE',
135
+ });
136
+ }
137
+ startWorkflow(workflowName) {
138
+ const id = this.workflowTracker.start(workflowName);
139
+ this.trackEvent('WORKFLOW_STARTED', {
140
+ workflowId: id,
141
+ workflowName,
142
+ });
143
+ return id;
144
+ }
145
+ completeWorkflow(workflowId) {
146
+ const result = this.workflowTracker.complete(workflowId);
147
+ if (result) {
148
+ this.trackEvent('WORKFLOW_COMPLETED', {
149
+ workflowId,
150
+ workflowName: result.name,
151
+ durationMs: result.durationMs,
152
+ });
153
+ }
154
+ }
155
+ failWorkflow(workflowId, reason) {
156
+ const result = this.workflowTracker.fail(workflowId);
157
+ if (result) {
158
+ this.trackEvent('WORKFLOW_FAILED', {
159
+ workflowId,
160
+ workflowName: result.name,
161
+ durationMs: result.durationMs,
162
+ reason: reason || 'Unknown error',
163
+ });
164
+ }
165
+ }
166
+ abandonWorkflow(workflowId) {
167
+ this.workflowTracker.abandon(workflowId);
168
+ }
169
+ cancelWorkflow(workflowId, reason) {
170
+ const result = this.workflowTracker.fail(workflowId);
171
+ if (result) {
172
+ this.trackEvent('WORKFLOW_CANCELLED', {
173
+ workflowId,
174
+ workflowName: result.name,
175
+ durationMs: result.durationMs,
176
+ reason: reason ?? 'Cancelled',
177
+ });
178
+ }
179
+ }
180
+ captureException(error, context) {
181
+ const err = error instanceof Error ? error : new Error(String(error));
182
+ this.trackEvent('ERROR_OCCURRED', {
183
+ message: err.message,
184
+ stack: err.stack || null,
185
+ name: err.name,
186
+ context: context || {},
187
+ });
188
+ }
189
+ captureMessage(message, severity = 'error') {
190
+ this.trackEvent('CLIENT_ERROR', {
191
+ message,
192
+ severity,
193
+ });
194
+ }
195
+ identifyUser(userId, traits) {
196
+ this.trackEvent('BUSINESS_EVENT', {
197
+ businessEventType: 'USER_IDENTIFIED',
198
+ userId,
199
+ traits: traits || {},
200
+ });
201
+ }
202
+ startFlushInterval() {
203
+ const intervalMs = this.config?.flushIntervalMs || 5000;
204
+ this.flushInterval = window.setInterval(() => {
205
+ this.flush();
206
+ }, intervalMs);
207
+ }
208
+ async flush() {
209
+ if (this.eventBuffer.length === 0 || !this.config)
210
+ return;
211
+ const eventsToSend = [...this.eventBuffer];
212
+ this.eventBuffer = [];
213
+ try {
214
+ const payload = JSON.stringify(eventsToSend);
215
+ // Enforce 5 MB batch limit
216
+ const payloadSize = typeof Blob !== 'undefined'
217
+ ? new Blob([payload]).size
218
+ : payload.length;
219
+ if (payloadSize > 5 * 1024 * 1024) {
220
+ console.error(`[Tellann] Batch payload size of ${payloadSize} bytes exceeds the 5 MB limit. Dropping batch.`);
221
+ return;
222
+ }
223
+ const headers = {
224
+ 'Content-Type': 'application/json',
225
+ };
226
+ if (this.config.apiKey) {
227
+ headers.Authorization = `Bearer ${this.config.apiKey}`;
228
+ }
229
+ if (this.config.environmentId) {
230
+ headers['x-tellann-environment-id'] = this.config.environmentId;
231
+ }
232
+ if (this.config.runId)
233
+ headers['x-tellann-run-id'] = this.config.runId;
234
+ if (this.sessionId)
235
+ headers['x-tellann-session-id'] = this.sessionId;
236
+ if (this.config.traceId)
237
+ headers['x-tellann-trace-id'] = this.config.traceId;
238
+ // sendBeacon cannot set auth headers, so only use it for unauthenticated direct collector targets.
239
+ if (!this.config.apiKey && !this.config.environmentId && navigator.sendBeacon && typeof Blob !== 'undefined') {
240
+ const blob = new Blob([payload], { type: 'application/json' });
241
+ const success = navigator.sendBeacon(`${this.config.endpoint}/v1/events/batch`, blob);
242
+ if (!success) {
243
+ throw new Error('sendBeacon returned false');
244
+ }
245
+ }
246
+ else {
247
+ // Fallback to fetch
248
+ await fetch(`${this.config.endpoint}/v1/events/batch`, {
249
+ method: 'POST',
250
+ headers,
251
+ body: payload,
252
+ keepalive: true, // Use keepalive for page unloads if beacon is unavailable
253
+ });
254
+ }
255
+ }
256
+ catch (error) {
257
+ if (this.config.debug) {
258
+ console.error('[Tellann] Failed to flush events', error);
259
+ }
260
+ // Re-add to buffer on failure
261
+ this.eventBuffer = [...eventsToSend, ...this.eventBuffer];
262
+ }
263
+ }
264
+ }
265
+ export const TELLANN = new TellannFrontendSDK();
266
+ export { TellannFrontendSDK };
@@ -0,0 +1,13 @@
1
+ export declare class WorkflowTracker {
2
+ private workflows;
3
+ start(name: string): string;
4
+ complete(workflowId: string): {
5
+ name: string;
6
+ durationMs: number;
7
+ } | null;
8
+ fail(workflowId: string): {
9
+ name: string;
10
+ durationMs: number;
11
+ } | null;
12
+ abandon(workflowId: string): void;
13
+ }
@@ -0,0 +1,35 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ export class WorkflowTracker {
3
+ workflows = new Map();
4
+ start(name) {
5
+ const id = uuidv4();
6
+ this.workflows.set(id, {
7
+ name,
8
+ startedAt: Date.now()
9
+ });
10
+ return id;
11
+ }
12
+ complete(workflowId) {
13
+ const workflow = this.workflows.get(workflowId);
14
+ if (!workflow)
15
+ return null;
16
+ this.workflows.delete(workflowId);
17
+ return {
18
+ name: workflow.name,
19
+ durationMs: Date.now() - workflow.startedAt
20
+ };
21
+ }
22
+ fail(workflowId) {
23
+ const workflow = this.workflows.get(workflowId);
24
+ if (!workflow)
25
+ return null;
26
+ this.workflows.delete(workflowId);
27
+ return {
28
+ name: workflow.name,
29
+ durationMs: Date.now() - workflow.startedAt
30
+ };
31
+ }
32
+ abandon(workflowId) {
33
+ this.workflows.delete(workflowId);
34
+ }
35
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@tellann/frontend-sdk",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Tellann browser telemetry and QA-run correlation SDK",
6
+ "license": "UNLICENSED",
7
+ "main": "dist/index.js",
8
+ "module": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/Pellumi/monitor.git",
13
+ "directory": "packages/frontend-sdk"
14
+ },
15
+ "homepage": "https://github.com/Pellumi/monitor/tree/main/packages/frontend-sdk#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/Pellumi/monitor/issues"
18
+ },
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "!dist/**/*.test.js",
33
+ "!dist/**/*.test.d.ts"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "uuid": "^9.0.1"
40
+ },
41
+ "devDependencies": {
42
+ "@types/uuid": "^9.0.8",
43
+ "typescript": "^5.0.0"
44
+ },
45
+ "scripts": {
46
+ "build": "tsc",
47
+ "dev": "tsc -w",
48
+ "test": "node --test dist/index.test.js"
49
+ }
50
+ }