@upstash/redis-analytics 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/LICENSE +21 -0
- package/README.md +175 -0
- package/dist/backend-client.d.ts +38 -0
- package/dist/backend-client.js +189 -0
- package/dist/client.d.ts +27 -0
- package/dist/client.js +157 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +20 -0
- package/dist/protocol.d.ts +45 -0
- package/dist/protocol.js +4 -0
- package/dist/react.d.ts +33 -0
- package/dist/react.js +95 -0
- package/dist/services/events.d.ts +26 -0
- package/dist/services/events.js +143 -0
- package/dist/services/feature-flags.d.ts +14 -0
- package/dist/services/feature-flags.js +88 -0
- package/dist/services/logging.d.ts +13 -0
- package/dist/services/logging.js +66 -0
- package/dist/services/schema-registry.d.ts +15 -0
- package/dist/services/schema-registry.js +97 -0
- package/dist/services/search-index.d.ts +35 -0
- package/dist/services/search-index.js +293 -0
- package/dist/services/sessions.d.ts +18 -0
- package/dist/services/sessions.js +58 -0
- package/dist/types.d.ts +144 -0
- package/dist/types.js +2 -0
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +44 -0
- package/package.json +36 -0
- package/src/backend-client.ts +301 -0
- package/src/client.ts +245 -0
- package/src/index.ts +39 -0
- package/src/protocol.ts +57 -0
- package/src/react.ts +163 -0
- package/src/services/events.ts +187 -0
- package/src/services/feature-flags.ts +125 -0
- package/src/services/logging.ts +81 -0
- package/src/services/schema-registry.ts +125 -0
- package/src/services/search-index.ts +335 -0
- package/src/services/sessions.ts +86 -0
- package/src/types.ts +194 -0
- package/src/utils.ts +45 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { Redis } from "@upstash/redis";
|
|
2
|
+
import type {
|
|
3
|
+
AnalyticsConfig,
|
|
4
|
+
CaptureEventInput,
|
|
5
|
+
DeepPartial,
|
|
6
|
+
EventSchema,
|
|
7
|
+
FeatureFlagAssignment,
|
|
8
|
+
FeatureFlagConfig,
|
|
9
|
+
FeatureFlagDefinition,
|
|
10
|
+
LogType,
|
|
11
|
+
SearchIndexInfo,
|
|
12
|
+
ServerConfig,
|
|
13
|
+
Session,
|
|
14
|
+
SessionResult,
|
|
15
|
+
SystemLog,
|
|
16
|
+
} from "./types";
|
|
17
|
+
import type { AnalyticsRequest, AnalyticsResponse } from "./protocol";
|
|
18
|
+
import { LoggingService } from "./services/logging";
|
|
19
|
+
import { FeatureFlagService } from "./services/feature-flags";
|
|
20
|
+
import { SessionService } from "./services/sessions";
|
|
21
|
+
import { EventService } from "./services/events";
|
|
22
|
+
import { SchemaRegistry } from "./services/schema-registry";
|
|
23
|
+
import { SearchIndexService } from "./services/search-index";
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CONFIG: AnalyticsConfig = {
|
|
26
|
+
session: {
|
|
27
|
+
expirationMs: 3_600_000, // 1 hour
|
|
28
|
+
},
|
|
29
|
+
featureFlags: {},
|
|
30
|
+
events: {
|
|
31
|
+
customEventRetentionDays: 7,
|
|
32
|
+
maxBatchSize: 20,
|
|
33
|
+
},
|
|
34
|
+
schemaValidation: {
|
|
35
|
+
checkFrequency: 1,
|
|
36
|
+
},
|
|
37
|
+
logging: {
|
|
38
|
+
retentionDays: 30,
|
|
39
|
+
enabledTypes: [
|
|
40
|
+
"schema_update",
|
|
41
|
+
"feature_flag_update",
|
|
42
|
+
"system_error",
|
|
43
|
+
"index_update",
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
search: {
|
|
47
|
+
indexName: "events-idx",
|
|
48
|
+
autoUpdate: true,
|
|
49
|
+
rebuildOnStartup: false,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function deepMerge<T extends Record<string, unknown>>(
|
|
54
|
+
target: T,
|
|
55
|
+
source: DeepPartial<T>
|
|
56
|
+
): T {
|
|
57
|
+
const result = { ...target };
|
|
58
|
+
for (const key of Object.keys(source) as Array<keyof T>) {
|
|
59
|
+
const sourceVal = source[key];
|
|
60
|
+
if (sourceVal === undefined) continue;
|
|
61
|
+
const targetVal = result[key];
|
|
62
|
+
if (
|
|
63
|
+
targetVal &&
|
|
64
|
+
typeof targetVal === "object" &&
|
|
65
|
+
!Array.isArray(targetVal) &&
|
|
66
|
+
typeof sourceVal === "object" &&
|
|
67
|
+
!Array.isArray(sourceVal)
|
|
68
|
+
) {
|
|
69
|
+
result[key] = deepMerge(
|
|
70
|
+
targetVal as Record<string, unknown>,
|
|
71
|
+
sourceVal as Record<string, unknown>
|
|
72
|
+
) as T[keyof T];
|
|
73
|
+
} else {
|
|
74
|
+
result[key] = sourceVal as T[keyof T];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type MiddlewareResult<
|
|
81
|
+
TSessionMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
82
|
+
> = {
|
|
83
|
+
sessionMetadata: TSessionMetadata;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type HandlerOptions<
|
|
87
|
+
TSessionMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
88
|
+
> = {
|
|
89
|
+
/**
|
|
90
|
+
* Middleware that runs before each request is processed.
|
|
91
|
+
* Return a Response to short-circuit (e.g. 401 Unauthorized).
|
|
92
|
+
* Return void to let the request proceed.
|
|
93
|
+
* Return { sessionMetadata } to attach metadata to the session (createSession only).
|
|
94
|
+
*/
|
|
95
|
+
middleware?: (context: {
|
|
96
|
+
request: Request;
|
|
97
|
+
analyticsRequest: AnalyticsRequest;
|
|
98
|
+
}) =>
|
|
99
|
+
| Response
|
|
100
|
+
| void
|
|
101
|
+
| MiddlewareResult<TSessionMetadata>
|
|
102
|
+
| Promise<Response | void | MiddlewareResult<TSessionMetadata>>;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export class AnalyticsBackendClient<
|
|
106
|
+
TCustomEvents extends Record<string, Record<string, unknown>> = Record<
|
|
107
|
+
string,
|
|
108
|
+
Record<string, unknown>
|
|
109
|
+
>,
|
|
110
|
+
TFeatureFlags extends Record<string, string> = Record<string, string>,
|
|
111
|
+
TSessionMetadata extends Record<string, unknown> = Record<string, unknown>,
|
|
112
|
+
> {
|
|
113
|
+
readonly events: EventService;
|
|
114
|
+
readonly sessions: SessionService;
|
|
115
|
+
readonly featureFlags: FeatureFlagService;
|
|
116
|
+
readonly schemas: SchemaRegistry;
|
|
117
|
+
readonly logs: LoggingService;
|
|
118
|
+
readonly searchIndex: SearchIndexService;
|
|
119
|
+
|
|
120
|
+
private redis: Redis;
|
|
121
|
+
private config: AnalyticsConfig;
|
|
122
|
+
|
|
123
|
+
constructor(serverConfig: ServerConfig) {
|
|
124
|
+
this.redis = new Redis({
|
|
125
|
+
url: serverConfig.redis.url,
|
|
126
|
+
token: serverConfig.redis.token,
|
|
127
|
+
});
|
|
128
|
+
this.config = deepMerge(
|
|
129
|
+
DEFAULT_CONFIG,
|
|
130
|
+
(serverConfig.config ?? {}) as DeepPartial<AnalyticsConfig>
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
this.logs = new LoggingService(this.redis, this.config);
|
|
134
|
+
this.featureFlags = new FeatureFlagService(this.redis, this.config, this.logs);
|
|
135
|
+
this.sessions = new SessionService(
|
|
136
|
+
this.redis,
|
|
137
|
+
this.config,
|
|
138
|
+
this.featureFlags,
|
|
139
|
+
this.logs
|
|
140
|
+
);
|
|
141
|
+
this.schemas = new SchemaRegistry(this.redis, this.config, this.logs);
|
|
142
|
+
this.searchIndex = new SearchIndexService(
|
|
143
|
+
this.redis,
|
|
144
|
+
this.config,
|
|
145
|
+
this.schemas,
|
|
146
|
+
this.featureFlags,
|
|
147
|
+
this.logs
|
|
148
|
+
);
|
|
149
|
+
this.events = new EventService(
|
|
150
|
+
this.redis,
|
|
151
|
+
this.config,
|
|
152
|
+
this.schemas,
|
|
153
|
+
this.logs
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ─── Handler ──────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
getHandler(options?: HandlerOptions<TSessionMetadata>): (request: Request) => Promise<Response> {
|
|
160
|
+
return (request: Request) => this.handleRequest(request, options);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async handleRequest(
|
|
164
|
+
request: Request,
|
|
165
|
+
options?: HandlerOptions<TSessionMetadata>
|
|
166
|
+
): Promise<Response> {
|
|
167
|
+
try {
|
|
168
|
+
const method = request.method.toUpperCase();
|
|
169
|
+
let analyticsRequest: AnalyticsRequest;
|
|
170
|
+
|
|
171
|
+
if (method === "GET") {
|
|
172
|
+
const url = new URL(request.url);
|
|
173
|
+
const requestType = url.searchParams.get("requestType");
|
|
174
|
+
|
|
175
|
+
if (!requestType) {
|
|
176
|
+
return new Response(
|
|
177
|
+
JSON.stringify({
|
|
178
|
+
success: false,
|
|
179
|
+
error: "Missing requestType query parameter",
|
|
180
|
+
}),
|
|
181
|
+
{ status: 400, headers: { "Content-Type": "application/json" } }
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
analyticsRequest = this.parseGetRequest(requestType, url.searchParams);
|
|
186
|
+
} else {
|
|
187
|
+
analyticsRequest = (await request.json()) as AnalyticsRequest;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Run middleware
|
|
191
|
+
let sessionMetadata: TSessionMetadata | undefined;
|
|
192
|
+
if (options?.middleware) {
|
|
193
|
+
const middlewareResult = await options.middleware({
|
|
194
|
+
request,
|
|
195
|
+
analyticsRequest,
|
|
196
|
+
});
|
|
197
|
+
if (middlewareResult instanceof Response) {
|
|
198
|
+
return middlewareResult;
|
|
199
|
+
}
|
|
200
|
+
if (middlewareResult && typeof middlewareResult === "object" && "sessionMetadata" in middlewareResult) {
|
|
201
|
+
sessionMetadata = middlewareResult.sessionMetadata;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const result = await this.processRequest(analyticsRequest, sessionMetadata);
|
|
206
|
+
return new Response(JSON.stringify(result), {
|
|
207
|
+
status: result.success ? 200 : 400,
|
|
208
|
+
headers: { "Content-Type": "application/json" },
|
|
209
|
+
});
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message =
|
|
212
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
213
|
+
const result: AnalyticsResponse = { success: false, error: message };
|
|
214
|
+
return new Response(JSON.stringify(result), {
|
|
215
|
+
status: 400,
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private parseGetRequest(
|
|
222
|
+
requestType: string,
|
|
223
|
+
params: URLSearchParams
|
|
224
|
+
): AnalyticsRequest {
|
|
225
|
+
switch (requestType) {
|
|
226
|
+
case "getSession":
|
|
227
|
+
return {
|
|
228
|
+
requestType: "getSession",
|
|
229
|
+
data: { sessionId: params.get("sessionId")! },
|
|
230
|
+
};
|
|
231
|
+
case "getFeatureFlag":
|
|
232
|
+
return {
|
|
233
|
+
requestType: "getFeatureFlag",
|
|
234
|
+
data: {
|
|
235
|
+
sessionId: params.get("sessionId")!,
|
|
236
|
+
flagName: params.get("flagName")!,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
case "getAllFeatureFlags":
|
|
240
|
+
return {
|
|
241
|
+
requestType: "getAllFeatureFlags",
|
|
242
|
+
data: { sessionId: params.get("sessionId")! },
|
|
243
|
+
};
|
|
244
|
+
default:
|
|
245
|
+
throw new Error(`Unknown GET request type: ${requestType}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private async processRequest(
|
|
250
|
+
req: AnalyticsRequest,
|
|
251
|
+
sessionMetadata?: Record<string, unknown>
|
|
252
|
+
): Promise<AnalyticsResponse> {
|
|
253
|
+
switch (req.requestType) {
|
|
254
|
+
case "captureEvent": {
|
|
255
|
+
await this.events.captureEvent(req.data as any);
|
|
256
|
+
return { success: true };
|
|
257
|
+
}
|
|
258
|
+
case "captureBatchEvents": {
|
|
259
|
+
await this.events.captureBatchEvents(req.data.events);
|
|
260
|
+
return { success: true };
|
|
261
|
+
}
|
|
262
|
+
case "getSession": {
|
|
263
|
+
const session = await this.sessions.getSession(req.data.sessionId);
|
|
264
|
+
return { success: true, data: session };
|
|
265
|
+
}
|
|
266
|
+
case "getFeatureFlag": {
|
|
267
|
+
const value = await this.sessions.getFeatureFlag(
|
|
268
|
+
req.data.sessionId,
|
|
269
|
+
req.data.flagName
|
|
270
|
+
);
|
|
271
|
+
return { success: true, data: { value } };
|
|
272
|
+
}
|
|
273
|
+
case "getAllFeatureFlags": {
|
|
274
|
+
const flags = await this.sessions.getAllFeatureFlags(req.data.sessionId);
|
|
275
|
+
return { success: true, data: flags };
|
|
276
|
+
}
|
|
277
|
+
case "createSession": {
|
|
278
|
+
const session = await this.sessions.createSession({
|
|
279
|
+
featureFlags: req.data.featureFlags as
|
|
280
|
+
| Record<string, FeatureFlagAssignment>
|
|
281
|
+
| undefined,
|
|
282
|
+
metadata: sessionMetadata,
|
|
283
|
+
});
|
|
284
|
+
// Register session metadata schema so it's available in search index
|
|
285
|
+
if (sessionMetadata && Object.keys(sessionMetadata).length > 0) {
|
|
286
|
+
await this.schemas.validateAndUpdateSchema(
|
|
287
|
+
"__session_metadata__",
|
|
288
|
+
sessionMetadata
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
return { success: true, data: session };
|
|
292
|
+
}
|
|
293
|
+
default: {
|
|
294
|
+
return {
|
|
295
|
+
success: false,
|
|
296
|
+
error: `Unknown request type: ${(req as any).requestType}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnalyticsRequest,
|
|
3
|
+
AnalyticsResponse,
|
|
4
|
+
} from "./protocol";
|
|
5
|
+
import type {
|
|
6
|
+
CaptureEventInput,
|
|
7
|
+
ClientConfig,
|
|
8
|
+
Session,
|
|
9
|
+
SessionResult,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
export class AnalyticsClient<
|
|
13
|
+
TCustomEvents extends Record<string, Record<string, unknown>> = Record<
|
|
14
|
+
string,
|
|
15
|
+
Record<string, unknown>
|
|
16
|
+
>,
|
|
17
|
+
TFeatureFlags extends Record<string, string> = Record<string, string>,
|
|
18
|
+
> {
|
|
19
|
+
private endpoint: string;
|
|
20
|
+
private flushInterval: number;
|
|
21
|
+
private maxBatchSize: number;
|
|
22
|
+
private eventQueue: Array<{
|
|
23
|
+
sessionId: string;
|
|
24
|
+
eventName: string;
|
|
25
|
+
properties?: Record<string, unknown>;
|
|
26
|
+
}> = [];
|
|
27
|
+
private flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
28
|
+
|
|
29
|
+
constructor(config?: ClientConfig) {
|
|
30
|
+
this.endpoint = config?.endpoint ?? "/api/analytics";
|
|
31
|
+
this.flushInterval = config?.flushInterval ?? 0;
|
|
32
|
+
this.maxBatchSize = config?.maxBatchSize ?? 20;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async sendPost<T = unknown>(
|
|
36
|
+
request: AnalyticsRequest
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
const response = await fetch(this.endpoint, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify(request),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const result: AnalyticsResponse = await response.json();
|
|
45
|
+
|
|
46
|
+
if (!result.success) {
|
|
47
|
+
throw new Error(result.error ?? "Analytics request failed");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return result.data as T;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async sendGet<T = unknown>(
|
|
54
|
+
requestType: string,
|
|
55
|
+
params: Record<string, string>
|
|
56
|
+
): Promise<T> {
|
|
57
|
+
const searchParams = new URLSearchParams({ requestType, ...params });
|
|
58
|
+
const response = await fetch(`${this.endpoint}?${searchParams}`);
|
|
59
|
+
|
|
60
|
+
const result: AnalyticsResponse = await response.json();
|
|
61
|
+
|
|
62
|
+
if (!result.success) {
|
|
63
|
+
throw new Error(result.error ?? "Analytics request failed");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return result.data as T;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ─── Batch Queue ─────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
private enqueueEvent(event: {
|
|
72
|
+
sessionId: string;
|
|
73
|
+
eventName: string;
|
|
74
|
+
properties?: Record<string, unknown>;
|
|
75
|
+
}): void {
|
|
76
|
+
this.eventQueue.push(event);
|
|
77
|
+
|
|
78
|
+
if (this.flushTimer) {
|
|
79
|
+
clearTimeout(this.flushTimer);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.flushTimer = setTimeout(() => {
|
|
83
|
+
this.flush();
|
|
84
|
+
}, this.flushInterval);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async flush(): Promise<void> {
|
|
88
|
+
if (this.flushTimer) {
|
|
89
|
+
clearTimeout(this.flushTimer);
|
|
90
|
+
this.flushTimer = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (this.eventQueue.length === 0) return;
|
|
94
|
+
|
|
95
|
+
const events = [...this.eventQueue];
|
|
96
|
+
this.eventQueue = [];
|
|
97
|
+
|
|
98
|
+
if (events.length === 1) {
|
|
99
|
+
await this.sendPost({
|
|
100
|
+
requestType: "captureEvent",
|
|
101
|
+
data: events[0],
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
// Send in chunks of maxBatchSize
|
|
105
|
+
for (let i = 0; i < events.length; i += this.maxBatchSize) {
|
|
106
|
+
const chunk = events.slice(i, i + this.maxBatchSize);
|
|
107
|
+
await this.sendPost({
|
|
108
|
+
requestType: "captureBatchEvents",
|
|
109
|
+
data: { events: chunk },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Session Management ──────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
async createSession(options?: {
|
|
118
|
+
featureFlags?: { [K in keyof TFeatureFlags]?: TFeatureFlags[K] | Record<string, number> };
|
|
119
|
+
}): Promise<Session<TFeatureFlags>> {
|
|
120
|
+
return this.sendPost<Session<TFeatureFlags>>({
|
|
121
|
+
requestType: "createSession",
|
|
122
|
+
data: {
|
|
123
|
+
featureFlags: options?.featureFlags as Record<string, string | Record<string, number>> | undefined,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async getSession(sessionId: string): Promise<SessionResult<TFeatureFlags>> {
|
|
129
|
+
return this.sendGet<SessionResult<TFeatureFlags>>("getSession", { sessionId });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Event Tracking ──────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
async captureEvent(
|
|
135
|
+
input: CaptureEventInput<TCustomEvents>
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
const event = input as {
|
|
138
|
+
sessionId: string;
|
|
139
|
+
eventName: string;
|
|
140
|
+
properties?: Record<string, unknown>;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (this.flushInterval > 0) {
|
|
144
|
+
this.enqueueEvent(event);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await this.sendPost({
|
|
149
|
+
requestType: "captureEvent",
|
|
150
|
+
data: event,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async capturePageView(sessionId: string, path: string): Promise<void> {
|
|
155
|
+
const event = { sessionId, eventName: "pageview" as const, properties: { path } };
|
|
156
|
+
|
|
157
|
+
if (this.flushInterval > 0) {
|
|
158
|
+
this.enqueueEvent(event);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await this.sendPost({
|
|
163
|
+
requestType: "captureEvent",
|
|
164
|
+
data: event,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async captureClick(sessionId: string, element: string): Promise<void> {
|
|
169
|
+
const event = { sessionId, eventName: "click" as const, properties: { element } };
|
|
170
|
+
|
|
171
|
+
if (this.flushInterval > 0) {
|
|
172
|
+
this.enqueueEvent(event);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await this.sendPost({
|
|
177
|
+
requestType: "captureEvent",
|
|
178
|
+
data: event,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async captureError(sessionId: string, message: string): Promise<void> {
|
|
183
|
+
const event = { sessionId, eventName: "error" as const, properties: { message } };
|
|
184
|
+
|
|
185
|
+
if (this.flushInterval > 0) {
|
|
186
|
+
this.enqueueEvent(event);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await this.sendPost({
|
|
191
|
+
requestType: "captureEvent",
|
|
192
|
+
data: event,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async captureWarning(sessionId: string, message: string): Promise<void> {
|
|
197
|
+
const event = { sessionId, eventName: "warning" as const, properties: { message } };
|
|
198
|
+
|
|
199
|
+
if (this.flushInterval > 0) {
|
|
200
|
+
this.enqueueEvent(event);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await this.sendPost({
|
|
205
|
+
requestType: "captureEvent",
|
|
206
|
+
data: event,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async captureInfo(sessionId: string, message: string): Promise<void> {
|
|
211
|
+
const event = { sessionId, eventName: "info" as const, properties: { message } };
|
|
212
|
+
|
|
213
|
+
if (this.flushInterval > 0) {
|
|
214
|
+
this.enqueueEvent(event);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await this.sendPost({
|
|
219
|
+
requestType: "captureEvent",
|
|
220
|
+
data: event,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ─── Feature Flags ───────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
async getFeatureFlag<K extends string & keyof TFeatureFlags>(
|
|
227
|
+
sessionId: string,
|
|
228
|
+
flagName: K
|
|
229
|
+
): Promise<TFeatureFlags[K] | null> {
|
|
230
|
+
const result = await this.sendGet<{ value: TFeatureFlags[K] | null }>(
|
|
231
|
+
"getFeatureFlag",
|
|
232
|
+
{ sessionId, flagName }
|
|
233
|
+
);
|
|
234
|
+
return result.value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async getAllFeatureFlags(
|
|
238
|
+
sessionId: string
|
|
239
|
+
): Promise<TFeatureFlags> {
|
|
240
|
+
return this.sendGet<TFeatureFlags>(
|
|
241
|
+
"getAllFeatureFlags",
|
|
242
|
+
{ sessionId }
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export { AnalyticsBackendClient } from "./backend-client";
|
|
2
|
+
export type { HandlerOptions, MiddlewareResult } from "./backend-client";
|
|
3
|
+
export { AnalyticsClient } from "./client";
|
|
4
|
+
|
|
5
|
+
// Services (for advanced usage)
|
|
6
|
+
export { EventService } from "./services/events";
|
|
7
|
+
export { SessionService } from "./services/sessions";
|
|
8
|
+
export { FeatureFlagService } from "./services/feature-flags";
|
|
9
|
+
export { SchemaRegistry } from "./services/schema-registry";
|
|
10
|
+
export { SearchIndexService } from "./services/search-index";
|
|
11
|
+
export { LoggingService } from "./services/logging";
|
|
12
|
+
|
|
13
|
+
// Types
|
|
14
|
+
export type {
|
|
15
|
+
AnalyticsConfig,
|
|
16
|
+
CaptureEventInput,
|
|
17
|
+
ClientConfig,
|
|
18
|
+
DeepPartial,
|
|
19
|
+
EventData,
|
|
20
|
+
EventSchema,
|
|
21
|
+
FeatureFlagAssignment,
|
|
22
|
+
FeatureFlagConfig,
|
|
23
|
+
FeatureFlagDefinition,
|
|
24
|
+
FeatureFlagDefinitions,
|
|
25
|
+
LogType,
|
|
26
|
+
SchemaProperty,
|
|
27
|
+
SchemaPropertyType,
|
|
28
|
+
SearchIndexInfo,
|
|
29
|
+
SDKOptions,
|
|
30
|
+
ServerConfig,
|
|
31
|
+
Session,
|
|
32
|
+
SessionMetadata,
|
|
33
|
+
SessionResult,
|
|
34
|
+
StandardEventMap,
|
|
35
|
+
StandardEventName,
|
|
36
|
+
SystemLog,
|
|
37
|
+
} from "./types";
|
|
38
|
+
|
|
39
|
+
export type { AnalyticsRequest, AnalyticsResponse } from "./protocol";
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Protocol types for communication between frontend client and backend server.
|
|
2
|
+
// These define the request/response contract for the single analytics endpoint.
|
|
3
|
+
|
|
4
|
+
export type AnalyticsRequest =
|
|
5
|
+
| {
|
|
6
|
+
requestType: "captureEvent";
|
|
7
|
+
data: {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
eventName: string;
|
|
10
|
+
properties?: Record<string, unknown>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
requestType: "captureBatchEvents";
|
|
15
|
+
data: {
|
|
16
|
+
events: Array<{
|
|
17
|
+
sessionId: string;
|
|
18
|
+
eventName: string;
|
|
19
|
+
properties?: Record<string, unknown>;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
requestType: "getSession";
|
|
25
|
+
data: {
|
|
26
|
+
sessionId: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
requestType: "getFeatureFlag";
|
|
31
|
+
data: {
|
|
32
|
+
sessionId: string;
|
|
33
|
+
flagName: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
requestType: "getAllFeatureFlags";
|
|
38
|
+
data: {
|
|
39
|
+
sessionId: string;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
| {
|
|
43
|
+
requestType: "createSession";
|
|
44
|
+
data: {
|
|
45
|
+
featureFlags?: Record<string, string | Record<string, number>>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type AnalyticsResponse =
|
|
50
|
+
| {
|
|
51
|
+
success: true;
|
|
52
|
+
data?: unknown;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
success: false;
|
|
56
|
+
error: string;
|
|
57
|
+
};
|