@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Upstash, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# @upstash/redis-analytics
|
|
2
|
+
|
|
3
|
+
Unified analytics package for Upstash Redis. Provides both server-side (`AnalyticsBackendClient`) and browser-side (`AnalyticsClient`) functionality in a single package.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This package is the complete analytics solution. It provides:
|
|
8
|
+
|
|
9
|
+
- Session management (creation, retrieval, validation)
|
|
10
|
+
- Event tracking and capture (standard + custom events)
|
|
11
|
+
- Feature flag management with A/B testing
|
|
12
|
+
- Redis Search index management (blue-green deployment)
|
|
13
|
+
- Custom event schema registry
|
|
14
|
+
- System logging
|
|
15
|
+
- Rate limiting
|
|
16
|
+
- Browser-side client with event batching
|
|
17
|
+
|
|
18
|
+
## Architecture
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
┌─────────────────────────────────────┐
|
|
22
|
+
│ Applications │
|
|
23
|
+
│ (ui, app, user apps) │
|
|
24
|
+
└──────────────┬──────────────────────┘
|
|
25
|
+
│
|
|
26
|
+
▼
|
|
27
|
+
┌─────────────────────────────────────┐
|
|
28
|
+
│ @upstash/redis-analytics │
|
|
29
|
+
│ AnalyticsBackendClient (server) │
|
|
30
|
+
│ AnalyticsClient (browser) │
|
|
31
|
+
└──────────────┬──────────────────────┘
|
|
32
|
+
│
|
|
33
|
+
▼
|
|
34
|
+
┌─────────────────────────────────────┐
|
|
35
|
+
│ Upstash Redis + Redis Search │
|
|
36
|
+
└─────────────────────────────────────┘
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Package Structure
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
src/
|
|
43
|
+
index.ts # barrel exports
|
|
44
|
+
backend-client.ts # AnalyticsBackendClient (server-side)
|
|
45
|
+
client.ts # AnalyticsClient (browser-side)
|
|
46
|
+
types.ts # all shared types
|
|
47
|
+
protocol.ts # request/response contract
|
|
48
|
+
utils.ts # helpers
|
|
49
|
+
services/
|
|
50
|
+
events.ts # EventService
|
|
51
|
+
sessions.ts # SessionService
|
|
52
|
+
feature-flags.ts # FeatureFlagService
|
|
53
|
+
schema-registry.ts # SchemaRegistry
|
|
54
|
+
search-index.ts # SearchIndexService
|
|
55
|
+
logging.ts # LoggingService
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
### Server-side (Next.js API route)
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { AnalyticsBackendClient } from "@upstash/redis-analytics";
|
|
64
|
+
|
|
65
|
+
const client = new AnalyticsBackendClient({
|
|
66
|
+
redis: {
|
|
67
|
+
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
68
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
69
|
+
},
|
|
70
|
+
config: {
|
|
71
|
+
// All config is optional — only override what you need
|
|
72
|
+
session: { expirationMs: 7200000 },
|
|
73
|
+
schemaValidation: { checkFrequency: 0.01 }, // validate ~1% of events to reduce costs
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Single endpoint handler for Next.js
|
|
78
|
+
const handler = client.getHandler();
|
|
79
|
+
export const POST = handler;
|
|
80
|
+
export const GET = handler;
|
|
81
|
+
|
|
82
|
+
// Or use namespaced services directly
|
|
83
|
+
const session = await client.sessions.createSession();
|
|
84
|
+
await client.events.capturePageView(session.id, "/home");
|
|
85
|
+
const flags = await client.featureFlags.getDefinitions();
|
|
86
|
+
const logs = await client.logs.getLogs({ limit: 50 });
|
|
87
|
+
const info = await client.searchIndex.getInfo();
|
|
88
|
+
const schemas = await client.schemas.getAllSchemas();
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Browser-side (React/Next.js)
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { AnalyticsClient } from "@upstash/redis-analytics";
|
|
95
|
+
|
|
96
|
+
const analytics = new AnalyticsClient({
|
|
97
|
+
endpoint: "/api/analytics",
|
|
98
|
+
flushInterval: 2000, // batch events, flush every 2s
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const session = await analytics.createSession();
|
|
102
|
+
await analytics.capturePageView(session.id, "/home");
|
|
103
|
+
await analytics.captureClick(session.id, "signup-button");
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Middleware & Session Metadata
|
|
107
|
+
|
|
108
|
+
Add middleware to `getHandler()` for authentication and to attach metadata to sessions.
|
|
109
|
+
The 3rd generic parameter provides type safety for session metadata.
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { AnalyticsBackendClient } from "@upstash/redis-analytics";
|
|
113
|
+
|
|
114
|
+
type MyEvents = { "custom:purchase": { productId: string; amount: number } };
|
|
115
|
+
type MyFlags = { theme: "light" | "dark" };
|
|
116
|
+
type MySessionMetadata = { userId: string; plan: string };
|
|
117
|
+
|
|
118
|
+
const client = new AnalyticsBackendClient<MyEvents, MyFlags, MySessionMetadata>({
|
|
119
|
+
redis: { url: "...", token: "..." },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const handler = client.getHandler({
|
|
123
|
+
middleware: async ({ request, analyticsRequest }) => {
|
|
124
|
+
const token = request.headers.get("authorization");
|
|
125
|
+
if (!token) {
|
|
126
|
+
return new Response(JSON.stringify({ success: false, error: "Unauthorized" }), { status: 401 });
|
|
127
|
+
}
|
|
128
|
+
const user = await verifyToken(token);
|
|
129
|
+
// Attach metadata to the session — stored in Redis, included in every event
|
|
130
|
+
return { sessionMetadata: { userId: user.id, plan: user.plan } };
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const POST = handler;
|
|
135
|
+
export const GET = handler;
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Session metadata is automatically:
|
|
139
|
+
- Stored with the session in Redis
|
|
140
|
+
- Included in every captured event under `sessionMetadata`
|
|
141
|
+
- Registered in the schema registry for search index queryability
|
|
142
|
+
|
|
143
|
+
### Type Safety
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { AnalyticsBackendClient, AnalyticsClient } from "@upstash/redis-analytics";
|
|
147
|
+
|
|
148
|
+
type MyEvents = {
|
|
149
|
+
"custom:purchase": { productId: string; amount: number };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
type MyFlags = {
|
|
153
|
+
theme: "light" | "dark";
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
type MySessionMetadata = {
|
|
157
|
+
userId: string;
|
|
158
|
+
plan: string;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Backend accepts 3 generic parameters: events, flags, session metadata
|
|
162
|
+
const backend = new AnalyticsBackendClient<MyEvents, MyFlags, MySessionMetadata>({ ... });
|
|
163
|
+
|
|
164
|
+
// Frontend client accepts events and flags
|
|
165
|
+
const frontend = new AnalyticsClient<MyEvents, MyFlags>({ ... });
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Dependencies
|
|
169
|
+
|
|
170
|
+
- `@upstash/redis` - Redis client with Redis Search support
|
|
171
|
+
- `@upstash/ratelimit` - Distributed rate limiting
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
This package is part of a monorepo using pnpm workspaces.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ServerConfig } from "./types";
|
|
2
|
+
import type { AnalyticsRequest } from "./protocol";
|
|
3
|
+
import { LoggingService } from "./services/logging";
|
|
4
|
+
import { FeatureFlagService } from "./services/feature-flags";
|
|
5
|
+
import { SessionService } from "./services/sessions";
|
|
6
|
+
import { EventService } from "./services/events";
|
|
7
|
+
import { SchemaRegistry } from "./services/schema-registry";
|
|
8
|
+
import { SearchIndexService } from "./services/search-index";
|
|
9
|
+
export type MiddlewareResult<TSessionMetadata extends Record<string, unknown> = Record<string, unknown>> = {
|
|
10
|
+
sessionMetadata: TSessionMetadata;
|
|
11
|
+
};
|
|
12
|
+
export type HandlerOptions<TSessionMetadata extends Record<string, unknown> = Record<string, unknown>> = {
|
|
13
|
+
/**
|
|
14
|
+
* Middleware that runs before each request is processed.
|
|
15
|
+
* Return a Response to short-circuit (e.g. 401 Unauthorized).
|
|
16
|
+
* Return void to let the request proceed.
|
|
17
|
+
* Return { sessionMetadata } to attach metadata to the session (createSession only).
|
|
18
|
+
*/
|
|
19
|
+
middleware?: (context: {
|
|
20
|
+
request: Request;
|
|
21
|
+
analyticsRequest: AnalyticsRequest;
|
|
22
|
+
}) => Response | void | MiddlewareResult<TSessionMetadata> | Promise<Response | void | MiddlewareResult<TSessionMetadata>>;
|
|
23
|
+
};
|
|
24
|
+
export declare class AnalyticsBackendClient<TCustomEvents extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>, TFeatureFlags extends Record<string, string> = Record<string, string>, TSessionMetadata extends Record<string, unknown> = Record<string, unknown>> {
|
|
25
|
+
readonly events: EventService;
|
|
26
|
+
readonly sessions: SessionService;
|
|
27
|
+
readonly featureFlags: FeatureFlagService;
|
|
28
|
+
readonly schemas: SchemaRegistry;
|
|
29
|
+
readonly logs: LoggingService;
|
|
30
|
+
readonly searchIndex: SearchIndexService;
|
|
31
|
+
private redis;
|
|
32
|
+
private config;
|
|
33
|
+
constructor(serverConfig: ServerConfig);
|
|
34
|
+
getHandler(options?: HandlerOptions<TSessionMetadata>): (request: Request) => Promise<Response>;
|
|
35
|
+
private handleRequest;
|
|
36
|
+
private parseGetRequest;
|
|
37
|
+
private processRequest;
|
|
38
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AnalyticsBackendClient = void 0;
|
|
4
|
+
const redis_1 = require("@upstash/redis");
|
|
5
|
+
const logging_1 = require("./services/logging");
|
|
6
|
+
const feature_flags_1 = require("./services/feature-flags");
|
|
7
|
+
const sessions_1 = require("./services/sessions");
|
|
8
|
+
const events_1 = require("./services/events");
|
|
9
|
+
const schema_registry_1 = require("./services/schema-registry");
|
|
10
|
+
const search_index_1 = require("./services/search-index");
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
session: {
|
|
13
|
+
expirationMs: 3600000, // 1 hour
|
|
14
|
+
},
|
|
15
|
+
featureFlags: {},
|
|
16
|
+
events: {
|
|
17
|
+
customEventRetentionDays: 7,
|
|
18
|
+
maxBatchSize: 20,
|
|
19
|
+
},
|
|
20
|
+
schemaValidation: {
|
|
21
|
+
checkFrequency: 1,
|
|
22
|
+
},
|
|
23
|
+
logging: {
|
|
24
|
+
retentionDays: 30,
|
|
25
|
+
enabledTypes: [
|
|
26
|
+
"schema_update",
|
|
27
|
+
"feature_flag_update",
|
|
28
|
+
"system_error",
|
|
29
|
+
"index_update",
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
search: {
|
|
33
|
+
indexName: "events-idx",
|
|
34
|
+
autoUpdate: true,
|
|
35
|
+
rebuildOnStartup: false,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
function deepMerge(target, source) {
|
|
39
|
+
const result = { ...target };
|
|
40
|
+
for (const key of Object.keys(source)) {
|
|
41
|
+
const sourceVal = source[key];
|
|
42
|
+
if (sourceVal === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
const targetVal = result[key];
|
|
45
|
+
if (targetVal &&
|
|
46
|
+
typeof targetVal === "object" &&
|
|
47
|
+
!Array.isArray(targetVal) &&
|
|
48
|
+
typeof sourceVal === "object" &&
|
|
49
|
+
!Array.isArray(sourceVal)) {
|
|
50
|
+
result[key] = deepMerge(targetVal, sourceVal);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
result[key] = sourceVal;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
class AnalyticsBackendClient {
|
|
59
|
+
constructor(serverConfig) {
|
|
60
|
+
this.redis = new redis_1.Redis({
|
|
61
|
+
url: serverConfig.redis.url,
|
|
62
|
+
token: serverConfig.redis.token,
|
|
63
|
+
});
|
|
64
|
+
this.config = deepMerge(DEFAULT_CONFIG, (serverConfig.config ?? {}));
|
|
65
|
+
this.logs = new logging_1.LoggingService(this.redis, this.config);
|
|
66
|
+
this.featureFlags = new feature_flags_1.FeatureFlagService(this.redis, this.config, this.logs);
|
|
67
|
+
this.sessions = new sessions_1.SessionService(this.redis, this.config, this.featureFlags, this.logs);
|
|
68
|
+
this.schemas = new schema_registry_1.SchemaRegistry(this.redis, this.config, this.logs);
|
|
69
|
+
this.searchIndex = new search_index_1.SearchIndexService(this.redis, this.config, this.schemas, this.featureFlags, this.logs);
|
|
70
|
+
this.events = new events_1.EventService(this.redis, this.config, this.schemas, this.logs);
|
|
71
|
+
}
|
|
72
|
+
// ─── Handler ──────────────────────────────────────────────────
|
|
73
|
+
getHandler(options) {
|
|
74
|
+
return (request) => this.handleRequest(request, options);
|
|
75
|
+
}
|
|
76
|
+
async handleRequest(request, options) {
|
|
77
|
+
try {
|
|
78
|
+
const method = request.method.toUpperCase();
|
|
79
|
+
let analyticsRequest;
|
|
80
|
+
if (method === "GET") {
|
|
81
|
+
const url = new URL(request.url);
|
|
82
|
+
const requestType = url.searchParams.get("requestType");
|
|
83
|
+
if (!requestType) {
|
|
84
|
+
return new Response(JSON.stringify({
|
|
85
|
+
success: false,
|
|
86
|
+
error: "Missing requestType query parameter",
|
|
87
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
88
|
+
}
|
|
89
|
+
analyticsRequest = this.parseGetRequest(requestType, url.searchParams);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
analyticsRequest = (await request.json());
|
|
93
|
+
}
|
|
94
|
+
// Run middleware
|
|
95
|
+
let sessionMetadata;
|
|
96
|
+
if (options?.middleware) {
|
|
97
|
+
const middlewareResult = await options.middleware({
|
|
98
|
+
request,
|
|
99
|
+
analyticsRequest,
|
|
100
|
+
});
|
|
101
|
+
if (middlewareResult instanceof Response) {
|
|
102
|
+
return middlewareResult;
|
|
103
|
+
}
|
|
104
|
+
if (middlewareResult && typeof middlewareResult === "object" && "sessionMetadata" in middlewareResult) {
|
|
105
|
+
sessionMetadata = middlewareResult.sessionMetadata;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const result = await this.processRequest(analyticsRequest, sessionMetadata);
|
|
109
|
+
return new Response(JSON.stringify(result), {
|
|
110
|
+
status: result.success ? 200 : 400,
|
|
111
|
+
headers: { "Content-Type": "application/json" },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
116
|
+
const result = { success: false, error: message };
|
|
117
|
+
return new Response(JSON.stringify(result), {
|
|
118
|
+
status: 400,
|
|
119
|
+
headers: { "Content-Type": "application/json" },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
parseGetRequest(requestType, params) {
|
|
124
|
+
switch (requestType) {
|
|
125
|
+
case "getSession":
|
|
126
|
+
return {
|
|
127
|
+
requestType: "getSession",
|
|
128
|
+
data: { sessionId: params.get("sessionId") },
|
|
129
|
+
};
|
|
130
|
+
case "getFeatureFlag":
|
|
131
|
+
return {
|
|
132
|
+
requestType: "getFeatureFlag",
|
|
133
|
+
data: {
|
|
134
|
+
sessionId: params.get("sessionId"),
|
|
135
|
+
flagName: params.get("flagName"),
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
case "getAllFeatureFlags":
|
|
139
|
+
return {
|
|
140
|
+
requestType: "getAllFeatureFlags",
|
|
141
|
+
data: { sessionId: params.get("sessionId") },
|
|
142
|
+
};
|
|
143
|
+
default:
|
|
144
|
+
throw new Error(`Unknown GET request type: ${requestType}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async processRequest(req, sessionMetadata) {
|
|
148
|
+
switch (req.requestType) {
|
|
149
|
+
case "captureEvent": {
|
|
150
|
+
await this.events.captureEvent(req.data);
|
|
151
|
+
return { success: true };
|
|
152
|
+
}
|
|
153
|
+
case "captureBatchEvents": {
|
|
154
|
+
await this.events.captureBatchEvents(req.data.events);
|
|
155
|
+
return { success: true };
|
|
156
|
+
}
|
|
157
|
+
case "getSession": {
|
|
158
|
+
const session = await this.sessions.getSession(req.data.sessionId);
|
|
159
|
+
return { success: true, data: session };
|
|
160
|
+
}
|
|
161
|
+
case "getFeatureFlag": {
|
|
162
|
+
const value = await this.sessions.getFeatureFlag(req.data.sessionId, req.data.flagName);
|
|
163
|
+
return { success: true, data: { value } };
|
|
164
|
+
}
|
|
165
|
+
case "getAllFeatureFlags": {
|
|
166
|
+
const flags = await this.sessions.getAllFeatureFlags(req.data.sessionId);
|
|
167
|
+
return { success: true, data: flags };
|
|
168
|
+
}
|
|
169
|
+
case "createSession": {
|
|
170
|
+
const session = await this.sessions.createSession({
|
|
171
|
+
featureFlags: req.data.featureFlags,
|
|
172
|
+
metadata: sessionMetadata,
|
|
173
|
+
});
|
|
174
|
+
// Register session metadata schema so it's available in search index
|
|
175
|
+
if (sessionMetadata && Object.keys(sessionMetadata).length > 0) {
|
|
176
|
+
await this.schemas.validateAndUpdateSchema("__session_metadata__", sessionMetadata);
|
|
177
|
+
}
|
|
178
|
+
return { success: true, data: session };
|
|
179
|
+
}
|
|
180
|
+
default: {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: `Unknown request type: ${req.requestType}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
exports.AnalyticsBackendClient = AnalyticsBackendClient;
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CaptureEventInput, ClientConfig, Session, SessionResult } from "./types";
|
|
2
|
+
export declare class AnalyticsClient<TCustomEvents extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>, TFeatureFlags extends Record<string, string> = Record<string, string>> {
|
|
3
|
+
private endpoint;
|
|
4
|
+
private flushInterval;
|
|
5
|
+
private maxBatchSize;
|
|
6
|
+
private eventQueue;
|
|
7
|
+
private flushTimer;
|
|
8
|
+
constructor(config?: ClientConfig);
|
|
9
|
+
private sendPost;
|
|
10
|
+
private sendGet;
|
|
11
|
+
private enqueueEvent;
|
|
12
|
+
flush(): Promise<void>;
|
|
13
|
+
createSession(options?: {
|
|
14
|
+
featureFlags?: {
|
|
15
|
+
[K in keyof TFeatureFlags]?: TFeatureFlags[K] | Record<string, number>;
|
|
16
|
+
};
|
|
17
|
+
}): Promise<Session<TFeatureFlags>>;
|
|
18
|
+
getSession(sessionId: string): Promise<SessionResult<TFeatureFlags>>;
|
|
19
|
+
captureEvent(input: CaptureEventInput<TCustomEvents>): Promise<void>;
|
|
20
|
+
capturePageView(sessionId: string, path: string): Promise<void>;
|
|
21
|
+
captureClick(sessionId: string, element: string): Promise<void>;
|
|
22
|
+
captureError(sessionId: string, message: string): Promise<void>;
|
|
23
|
+
captureWarning(sessionId: string, message: string): Promise<void>;
|
|
24
|
+
captureInfo(sessionId: string, message: string): Promise<void>;
|
|
25
|
+
getFeatureFlag<K extends string & keyof TFeatureFlags>(sessionId: string, flagName: K): Promise<TFeatureFlags[K] | null>;
|
|
26
|
+
getAllFeatureFlags(sessionId: string): Promise<TFeatureFlags>;
|
|
27
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AnalyticsClient = void 0;
|
|
4
|
+
class AnalyticsClient {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.eventQueue = [];
|
|
7
|
+
this.flushTimer = null;
|
|
8
|
+
this.endpoint = config?.endpoint ?? "/api/analytics";
|
|
9
|
+
this.flushInterval = config?.flushInterval ?? 0;
|
|
10
|
+
this.maxBatchSize = config?.maxBatchSize ?? 20;
|
|
11
|
+
}
|
|
12
|
+
async sendPost(request) {
|
|
13
|
+
const response = await fetch(this.endpoint, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
headers: { "Content-Type": "application/json" },
|
|
16
|
+
body: JSON.stringify(request),
|
|
17
|
+
});
|
|
18
|
+
const result = await response.json();
|
|
19
|
+
if (!result.success) {
|
|
20
|
+
throw new Error(result.error ?? "Analytics request failed");
|
|
21
|
+
}
|
|
22
|
+
return result.data;
|
|
23
|
+
}
|
|
24
|
+
async sendGet(requestType, params) {
|
|
25
|
+
const searchParams = new URLSearchParams({ requestType, ...params });
|
|
26
|
+
const response = await fetch(`${this.endpoint}?${searchParams}`);
|
|
27
|
+
const result = await response.json();
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
throw new Error(result.error ?? "Analytics request failed");
|
|
30
|
+
}
|
|
31
|
+
return result.data;
|
|
32
|
+
}
|
|
33
|
+
// ─── Batch Queue ─────────────────────────────────────────────
|
|
34
|
+
enqueueEvent(event) {
|
|
35
|
+
this.eventQueue.push(event);
|
|
36
|
+
if (this.flushTimer) {
|
|
37
|
+
clearTimeout(this.flushTimer);
|
|
38
|
+
}
|
|
39
|
+
this.flushTimer = setTimeout(() => {
|
|
40
|
+
this.flush();
|
|
41
|
+
}, this.flushInterval);
|
|
42
|
+
}
|
|
43
|
+
async flush() {
|
|
44
|
+
if (this.flushTimer) {
|
|
45
|
+
clearTimeout(this.flushTimer);
|
|
46
|
+
this.flushTimer = null;
|
|
47
|
+
}
|
|
48
|
+
if (this.eventQueue.length === 0)
|
|
49
|
+
return;
|
|
50
|
+
const events = [...this.eventQueue];
|
|
51
|
+
this.eventQueue = [];
|
|
52
|
+
if (events.length === 1) {
|
|
53
|
+
await this.sendPost({
|
|
54
|
+
requestType: "captureEvent",
|
|
55
|
+
data: events[0],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
// Send in chunks of maxBatchSize
|
|
60
|
+
for (let i = 0; i < events.length; i += this.maxBatchSize) {
|
|
61
|
+
const chunk = events.slice(i, i + this.maxBatchSize);
|
|
62
|
+
await this.sendPost({
|
|
63
|
+
requestType: "captureBatchEvents",
|
|
64
|
+
data: { events: chunk },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// ─── Session Management ──────────────────────────────────────
|
|
70
|
+
async createSession(options) {
|
|
71
|
+
return this.sendPost({
|
|
72
|
+
requestType: "createSession",
|
|
73
|
+
data: {
|
|
74
|
+
featureFlags: options?.featureFlags,
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async getSession(sessionId) {
|
|
79
|
+
return this.sendGet("getSession", { sessionId });
|
|
80
|
+
}
|
|
81
|
+
// ─── Event Tracking ──────────────────────────────────────────
|
|
82
|
+
async captureEvent(input) {
|
|
83
|
+
const event = input;
|
|
84
|
+
if (this.flushInterval > 0) {
|
|
85
|
+
this.enqueueEvent(event);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
await this.sendPost({
|
|
89
|
+
requestType: "captureEvent",
|
|
90
|
+
data: event,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async capturePageView(sessionId, path) {
|
|
94
|
+
const event = { sessionId, eventName: "pageview", properties: { path } };
|
|
95
|
+
if (this.flushInterval > 0) {
|
|
96
|
+
this.enqueueEvent(event);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await this.sendPost({
|
|
100
|
+
requestType: "captureEvent",
|
|
101
|
+
data: event,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
async captureClick(sessionId, element) {
|
|
105
|
+
const event = { sessionId, eventName: "click", properties: { element } };
|
|
106
|
+
if (this.flushInterval > 0) {
|
|
107
|
+
this.enqueueEvent(event);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await this.sendPost({
|
|
111
|
+
requestType: "captureEvent",
|
|
112
|
+
data: event,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async captureError(sessionId, message) {
|
|
116
|
+
const event = { sessionId, eventName: "error", properties: { message } };
|
|
117
|
+
if (this.flushInterval > 0) {
|
|
118
|
+
this.enqueueEvent(event);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await this.sendPost({
|
|
122
|
+
requestType: "captureEvent",
|
|
123
|
+
data: event,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
async captureWarning(sessionId, message) {
|
|
127
|
+
const event = { sessionId, eventName: "warning", properties: { message } };
|
|
128
|
+
if (this.flushInterval > 0) {
|
|
129
|
+
this.enqueueEvent(event);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await this.sendPost({
|
|
133
|
+
requestType: "captureEvent",
|
|
134
|
+
data: event,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
async captureInfo(sessionId, message) {
|
|
138
|
+
const event = { sessionId, eventName: "info", properties: { message } };
|
|
139
|
+
if (this.flushInterval > 0) {
|
|
140
|
+
this.enqueueEvent(event);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
await this.sendPost({
|
|
144
|
+
requestType: "captureEvent",
|
|
145
|
+
data: event,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// ─── Feature Flags ───────────────────────────────────────────
|
|
149
|
+
async getFeatureFlag(sessionId, flagName) {
|
|
150
|
+
const result = await this.sendGet("getFeatureFlag", { sessionId, flagName });
|
|
151
|
+
return result.value;
|
|
152
|
+
}
|
|
153
|
+
async getAllFeatureFlags(sessionId) {
|
|
154
|
+
return this.sendGet("getAllFeatureFlags", { sessionId });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
exports.AnalyticsClient = AnalyticsClient;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { AnalyticsBackendClient } from "./backend-client";
|
|
2
|
+
export type { HandlerOptions, MiddlewareResult } from "./backend-client";
|
|
3
|
+
export { AnalyticsClient } from "./client";
|
|
4
|
+
export { EventService } from "./services/events";
|
|
5
|
+
export { SessionService } from "./services/sessions";
|
|
6
|
+
export { FeatureFlagService } from "./services/feature-flags";
|
|
7
|
+
export { SchemaRegistry } from "./services/schema-registry";
|
|
8
|
+
export { SearchIndexService } from "./services/search-index";
|
|
9
|
+
export { LoggingService } from "./services/logging";
|
|
10
|
+
export type { AnalyticsConfig, CaptureEventInput, ClientConfig, DeepPartial, EventData, EventSchema, FeatureFlagAssignment, FeatureFlagConfig, FeatureFlagDefinition, FeatureFlagDefinitions, LogType, SchemaProperty, SchemaPropertyType, SearchIndexInfo, SDKOptions, ServerConfig, Session, SessionMetadata, SessionResult, StandardEventMap, StandardEventName, SystemLog, } from "./types";
|
|
11
|
+
export type { AnalyticsRequest, AnalyticsResponse } from "./protocol";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LoggingService = exports.SearchIndexService = exports.SchemaRegistry = exports.FeatureFlagService = exports.SessionService = exports.EventService = exports.AnalyticsClient = exports.AnalyticsBackendClient = void 0;
|
|
4
|
+
var backend_client_1 = require("./backend-client");
|
|
5
|
+
Object.defineProperty(exports, "AnalyticsBackendClient", { enumerable: true, get: function () { return backend_client_1.AnalyticsBackendClient; } });
|
|
6
|
+
var client_1 = require("./client");
|
|
7
|
+
Object.defineProperty(exports, "AnalyticsClient", { enumerable: true, get: function () { return client_1.AnalyticsClient; } });
|
|
8
|
+
// Services (for advanced usage)
|
|
9
|
+
var events_1 = require("./services/events");
|
|
10
|
+
Object.defineProperty(exports, "EventService", { enumerable: true, get: function () { return events_1.EventService; } });
|
|
11
|
+
var sessions_1 = require("./services/sessions");
|
|
12
|
+
Object.defineProperty(exports, "SessionService", { enumerable: true, get: function () { return sessions_1.SessionService; } });
|
|
13
|
+
var feature_flags_1 = require("./services/feature-flags");
|
|
14
|
+
Object.defineProperty(exports, "FeatureFlagService", { enumerable: true, get: function () { return feature_flags_1.FeatureFlagService; } });
|
|
15
|
+
var schema_registry_1 = require("./services/schema-registry");
|
|
16
|
+
Object.defineProperty(exports, "SchemaRegistry", { enumerable: true, get: function () { return schema_registry_1.SchemaRegistry; } });
|
|
17
|
+
var search_index_1 = require("./services/search-index");
|
|
18
|
+
Object.defineProperty(exports, "SearchIndexService", { enumerable: true, get: function () { return search_index_1.SearchIndexService; } });
|
|
19
|
+
var logging_1 = require("./services/logging");
|
|
20
|
+
Object.defineProperty(exports, "LoggingService", { enumerable: true, get: function () { return logging_1.LoggingService; } });
|