@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.
Files changed (42) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -0
  3. package/dist/backend-client.d.ts +38 -0
  4. package/dist/backend-client.js +189 -0
  5. package/dist/client.d.ts +27 -0
  6. package/dist/client.js +157 -0
  7. package/dist/index.d.ts +11 -0
  8. package/dist/index.js +20 -0
  9. package/dist/protocol.d.ts +45 -0
  10. package/dist/protocol.js +4 -0
  11. package/dist/react.d.ts +33 -0
  12. package/dist/react.js +95 -0
  13. package/dist/services/events.d.ts +26 -0
  14. package/dist/services/events.js +143 -0
  15. package/dist/services/feature-flags.d.ts +14 -0
  16. package/dist/services/feature-flags.js +88 -0
  17. package/dist/services/logging.d.ts +13 -0
  18. package/dist/services/logging.js +66 -0
  19. package/dist/services/schema-registry.d.ts +15 -0
  20. package/dist/services/schema-registry.js +97 -0
  21. package/dist/services/search-index.d.ts +35 -0
  22. package/dist/services/search-index.js +293 -0
  23. package/dist/services/sessions.d.ts +18 -0
  24. package/dist/services/sessions.js +58 -0
  25. package/dist/types.d.ts +144 -0
  26. package/dist/types.js +2 -0
  27. package/dist/utils.d.ts +6 -0
  28. package/dist/utils.js +44 -0
  29. package/package.json +36 -0
  30. package/src/backend-client.ts +301 -0
  31. package/src/client.ts +245 -0
  32. package/src/index.ts +39 -0
  33. package/src/protocol.ts +57 -0
  34. package/src/react.ts +163 -0
  35. package/src/services/events.ts +187 -0
  36. package/src/services/feature-flags.ts +125 -0
  37. package/src/services/logging.ts +81 -0
  38. package/src/services/schema-registry.ts +125 -0
  39. package/src/services/search-index.ts +335 -0
  40. package/src/services/sessions.ts +86 -0
  41. package/src/types.ts +194 -0
  42. package/src/utils.ts +45 -0
@@ -0,0 +1,35 @@
1
+ import type { Redis } from "@upstash/redis";
2
+ import type { AnalyticsConfig, SearchIndexInfo } from "../types";
3
+ import { LoggingService } from "./logging";
4
+ import { SchemaRegistry } from "./schema-registry";
5
+ import { FeatureFlagService } from "./feature-flags";
6
+ export declare class SearchIndexService {
7
+ private redis;
8
+ private config;
9
+ private schemaRegistry;
10
+ private featureFlags;
11
+ private logger;
12
+ constructor(redis: Redis, config: AnalyticsConfig, schemaRegistry: SchemaRegistry, featureFlags: FeatureFlagService, logger: LoggingService);
13
+ private get indexName();
14
+ private get indexNameB();
15
+ private buildSchema;
16
+ private findAliasTarget;
17
+ private indexExists;
18
+ rebuild(): Promise<void>;
19
+ getInfo(): Promise<SearchIndexInfo>;
20
+ getActiveIndex(): import("@upstash/redis").SearchIndex<import("@upstash/redis").NestedIndexSchema | import("@upstash/redis").FlatIndexSchema>;
21
+ getActiveIndexWithFallback(): {
22
+ primary: import("@upstash/redis").SearchIndex<import("@upstash/redis").NestedIndexSchema | import("@upstash/redis").FlatIndexSchema>;
23
+ fallback: import("@upstash/redis").SearchIndex<import("@upstash/redis").NestedIndexSchema | import("@upstash/redis").FlatIndexSchema>;
24
+ };
25
+ /**
26
+ * Compares the expected schema (built from config + schema registry)
27
+ * against the actual index schema. Returns drift info.
28
+ */
29
+ checkSchemaDrift(): Promise<{
30
+ hasDrift: boolean;
31
+ missingFields: string[];
32
+ extraFields: string[];
33
+ indexExists: boolean;
34
+ }>;
35
+ }
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SearchIndexService = void 0;
4
+ const redis_1 = require("@upstash/redis");
5
+ class SearchIndexService {
6
+ constructor(redis, config, schemaRegistry, featureFlags, logger) {
7
+ this.redis = redis;
8
+ this.config = config;
9
+ this.schemaRegistry = schemaRegistry;
10
+ this.featureFlags = featureFlags;
11
+ this.logger = logger;
12
+ }
13
+ get indexName() {
14
+ return this.config.search.indexName;
15
+ }
16
+ get indexNameB() {
17
+ return `${this.indexName}-b`;
18
+ }
19
+ buildSchema(eventSchemas, featureFlagNames) {
20
+ // Build the schema object for Redis Search
21
+ // Using `any` for mixed field types since s.object accepts heterogeneous schemas
22
+ const schemaFields = {};
23
+ // Fixed fields
24
+ schemaFields["eventName"] = redis_1.s.keyword();
25
+ schemaFields["eventTime"] = redis_1.s.date().fast();
26
+ schemaFields["eventTimeMs"] = redis_1.s.number("F64");
27
+ schemaFields["sessionId"] = redis_1.s.string().noTokenize();
28
+ schemaFields["sessionCreationTimestamp"] = redis_1.s.date().fast();
29
+ // Feature flag fields from definitions stored in Redis
30
+ for (const flagName of featureFlagNames) {
31
+ schemaFields[`featureFlags.${flagName}`] = redis_1.s.string().noTokenize();
32
+ }
33
+ // Dynamic fields from event schemas
34
+ for (const [eventName, eventSchema] of Object.entries(eventSchemas)) {
35
+ // Session metadata schema uses sessionMetadata.* prefix
36
+ const prefix = eventName === "__session_metadata__" ? "sessionMetadata" : "properties";
37
+ for (const [propName, propDef] of Object.entries(eventSchema.properties)) {
38
+ const fieldName = `${prefix}.${propName}`;
39
+ if (schemaFields[fieldName])
40
+ continue; // Already defined
41
+ switch (propDef.type) {
42
+ case "string":
43
+ schemaFields[fieldName] = redis_1.s.string();
44
+ break;
45
+ case "number":
46
+ schemaFields[fieldName] = redis_1.s.number("F64");
47
+ break;
48
+ case "boolean":
49
+ schemaFields[fieldName] = redis_1.s.boolean();
50
+ break;
51
+ }
52
+ }
53
+ }
54
+ return redis_1.s.object(schemaFields);
55
+ }
56
+ async findAliasTarget() {
57
+ try {
58
+ const aliases = await this.redis.search.alias.list();
59
+ // aliases is a record of alias -> index name
60
+ const target = aliases[this.indexName];
61
+ return target ?? null;
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ }
67
+ async indexExists(name) {
68
+ try {
69
+ const index = this.redis.search.index({ name });
70
+ await index.describe();
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ async rebuild() {
78
+ const [eventSchemas, flagDefinitions] = await Promise.all([
79
+ this.schemaRegistry.getAllSchemas(),
80
+ this.featureFlags.getDefinitions(),
81
+ ]);
82
+ const schema = this.buildSchema(eventSchemas, Object.keys(flagDefinitions));
83
+ const aliasTarget = await this.findAliasTarget();
84
+ const indexAExists = await this.indexExists(this.indexName);
85
+ const indexBExists = await this.indexExists(this.indexNameB);
86
+ // Case 1: No index exists at all — create index A
87
+ if (!indexAExists && !indexBExists && !aliasTarget) {
88
+ const newIndex = await this.redis.search.createIndex({
89
+ name: this.indexName,
90
+ schema,
91
+ dataType: "json",
92
+ prefix: "event-",
93
+ existsOk: true,
94
+ });
95
+ await newIndex.waitIndexing();
96
+ await this.logger.log({
97
+ logType: "index_update",
98
+ metadata: { action: "create", indexName: this.indexName },
99
+ changes: {
100
+ before: null,
101
+ after: { activeIndex: this.indexName },
102
+ },
103
+ });
104
+ return;
105
+ }
106
+ // Case 2: Alias exists pointing to B — swap back to A
107
+ if (aliasTarget === this.indexNameB) {
108
+ const newIndex = await this.redis.search.createIndex({
109
+ name: this.indexName,
110
+ schema,
111
+ dataType: "json",
112
+ prefix: "event-",
113
+ existsOk: true,
114
+ });
115
+ await newIndex.waitIndexing();
116
+ // Delete alias and old index B
117
+ await this.redis.search.alias.delete({ alias: this.indexName });
118
+ try {
119
+ await this.redis.search.index({ name: this.indexNameB }).drop();
120
+ }
121
+ catch {
122
+ // Ignore if already gone
123
+ }
124
+ await this.logger.log({
125
+ logType: "index_update",
126
+ metadata: { action: "update", indexName: this.indexName },
127
+ changes: {
128
+ before: { activeIndex: this.indexNameB },
129
+ after: { activeIndex: this.indexName },
130
+ },
131
+ });
132
+ return;
133
+ }
134
+ // Case 3: No alias, index A exists — create B, alias to B, delete A
135
+ if (indexAExists && !aliasTarget) {
136
+ const newIndex = await this.redis.search.createIndex({
137
+ name: this.indexNameB,
138
+ schema,
139
+ dataType: "json",
140
+ prefix: "event-",
141
+ existsOk: true,
142
+ });
143
+ await newIndex.waitIndexing();
144
+ // Create alias pointing to B, then delete old index A
145
+ await this.redis.search.index({ name: this.indexName }).drop();
146
+ await this.redis.search.alias.add({
147
+ alias: this.indexName,
148
+ indexName: this.indexNameB,
149
+ });
150
+ await this.logger.log({
151
+ logType: "index_update",
152
+ metadata: { action: "update", indexName: this.indexNameB },
153
+ changes: {
154
+ before: { activeIndex: this.indexName },
155
+ after: { activeIndex: this.indexNameB },
156
+ },
157
+ });
158
+ return;
159
+ }
160
+ // Fallback: unexpected state — recreate from scratch
161
+ try {
162
+ await this.redis.search.alias.delete({ alias: this.indexName });
163
+ }
164
+ catch { /* no alias */ }
165
+ try {
166
+ await this.redis.search.index({ name: this.indexName }).drop();
167
+ }
168
+ catch { /* doesn't exist */ }
169
+ try {
170
+ await this.redis.search.index({ name: this.indexNameB }).drop();
171
+ }
172
+ catch { /* doesn't exist */ }
173
+ const newIndex = await this.redis.search.createIndex({
174
+ name: this.indexName,
175
+ schema,
176
+ dataType: "json",
177
+ prefix: "event-",
178
+ existsOk: true,
179
+ });
180
+ await newIndex.waitIndexing();
181
+ await this.logger.log({
182
+ logType: "index_update",
183
+ metadata: { action: "create", indexName: this.indexName },
184
+ changes: {
185
+ before: null,
186
+ after: { activeIndex: this.indexName },
187
+ },
188
+ });
189
+ }
190
+ async getInfo() {
191
+ // Try main index first, then B
192
+ for (const name of [this.indexName, this.indexNameB]) {
193
+ try {
194
+ const index = this.redis.search.index({ name });
195
+ const info = await index.describe();
196
+ if (!info)
197
+ continue;
198
+ const countResult = await index.count({ filter: {} });
199
+ return {
200
+ currentIndex: name,
201
+ alias: this.indexName,
202
+ schema: info.schema,
203
+ documentCount: countResult.count,
204
+ };
205
+ }
206
+ catch {
207
+ continue;
208
+ }
209
+ }
210
+ return {
211
+ currentIndex: "none",
212
+ alias: this.indexName,
213
+ schema: {},
214
+ documentCount: 0,
215
+ };
216
+ }
217
+ getActiveIndex() {
218
+ // Return an index reference for querying
219
+ return this.redis.search.index({ name: this.indexName });
220
+ }
221
+ getActiveIndexWithFallback() {
222
+ // Try main index, fallback to B
223
+ return {
224
+ primary: this.redis.search.index({ name: this.indexName }),
225
+ fallback: this.redis.search.index({ name: this.indexNameB }),
226
+ };
227
+ }
228
+ /**
229
+ * Compares the expected schema (built from config + schema registry)
230
+ * against the actual index schema. Returns drift info.
231
+ */
232
+ async checkSchemaDrift() {
233
+ const info = await this.getInfo();
234
+ if (info.currentIndex === "none") {
235
+ return {
236
+ hasDrift: false,
237
+ missingFields: [],
238
+ extraFields: [],
239
+ indexExists: false,
240
+ };
241
+ }
242
+ // Build expected schema from registries
243
+ const [eventSchemas, flagDefinitions] = await Promise.all([
244
+ this.schemaRegistry.getAllSchemas(),
245
+ this.featureFlags.getDefinitions(),
246
+ ]);
247
+ const expectedFields = new Set();
248
+ // Fixed fields
249
+ expectedFields.add("eventName");
250
+ expectedFields.add("eventTime");
251
+ expectedFields.add("eventTimeMs");
252
+ expectedFields.add("sessionId");
253
+ expectedFields.add("sessionCreationTimestamp");
254
+ // Feature flag fields
255
+ for (const flagName of Object.keys(flagDefinitions)) {
256
+ expectedFields.add(`featureFlags.${flagName}`);
257
+ }
258
+ // Dynamic fields from event schemas
259
+ for (const [eventName, eventSchema] of Object.entries(eventSchemas)) {
260
+ const prefix = eventName === "__session_metadata__" ? "sessionMetadata" : "properties";
261
+ for (const propName of Object.keys(eventSchema.properties)) {
262
+ expectedFields.add(`${prefix}.${propName}`);
263
+ }
264
+ }
265
+ // Get actual indexed fields from the schema info
266
+ // The schema from describe() uses $ prefix for JSON paths
267
+ const actualFields = new Set();
268
+ for (const key of Object.keys(info.schema)) {
269
+ // Normalize: remove leading "$." prefix if present
270
+ const normalized = key.startsWith("$.") ? key.slice(2) : key;
271
+ actualFields.add(normalized);
272
+ }
273
+ const missingFields = [];
274
+ for (const field of expectedFields) {
275
+ if (!actualFields.has(field)) {
276
+ missingFields.push(field);
277
+ }
278
+ }
279
+ const extraFields = [];
280
+ for (const field of actualFields) {
281
+ if (!expectedFields.has(field)) {
282
+ extraFields.push(field);
283
+ }
284
+ }
285
+ return {
286
+ hasDrift: missingFields.length > 0 || extraFields.length > 0,
287
+ missingFields,
288
+ extraFields,
289
+ indexExists: true,
290
+ };
291
+ }
292
+ }
293
+ exports.SearchIndexService = SearchIndexService;
@@ -0,0 +1,18 @@
1
+ import type { Redis } from "@upstash/redis";
2
+ import type { AnalyticsConfig, FeatureFlagAssignment, Session, SessionResult } from "../types";
3
+ import { FeatureFlagService } from "./feature-flags";
4
+ import { LoggingService } from "./logging";
5
+ export declare class SessionService {
6
+ private redis;
7
+ private config;
8
+ private featureFlags;
9
+ private logger;
10
+ constructor(redis: Redis, config: AnalyticsConfig, featureFlags: FeatureFlagService, logger: LoggingService);
11
+ createSession(options?: {
12
+ featureFlags?: Record<string, FeatureFlagAssignment>;
13
+ metadata?: Record<string, unknown>;
14
+ }): Promise<Session>;
15
+ getSession(sessionId: string): Promise<SessionResult>;
16
+ getFeatureFlag(sessionId: string, flagName: string): Promise<string | null>;
17
+ getAllFeatureFlags(sessionId: string): Promise<Record<string, string> | null>;
18
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SessionService = void 0;
4
+ const utils_1 = require("../utils");
5
+ const SESSION_PREFIX = "analytics:session:";
6
+ class SessionService {
7
+ constructor(redis, config, featureFlags, logger) {
8
+ this.redis = redis;
9
+ this.config = config;
10
+ this.featureFlags = featureFlags;
11
+ this.logger = logger;
12
+ }
13
+ async createSession(options) {
14
+ // Resolve feature flags
15
+ const resolvedFlags = await this.featureFlags.resolveFlags(options?.featureFlags);
16
+ const sessionId = (0, utils_1.generateSessionId)();
17
+ const createdAt = Date.now();
18
+ const sessionData = {
19
+ sessionId,
20
+ createdAt,
21
+ featureFlags: resolvedFlags,
22
+ ...(options?.metadata ? { metadata: options.metadata } : {}),
23
+ };
24
+ // Store session with expiration
25
+ const key = `${SESSION_PREFIX}${sessionId}`;
26
+ await this.redis.set(key, JSON.stringify(sessionData), {
27
+ px: this.config.session.expirationMs,
28
+ });
29
+ return {
30
+ id: sessionId,
31
+ createdAt,
32
+ featureFlags: resolvedFlags,
33
+ ...(options?.metadata ? { metadata: options.metadata } : {}),
34
+ };
35
+ }
36
+ async getSession(sessionId) {
37
+ const key = `${SESSION_PREFIX}${sessionId}`;
38
+ const data = await this.redis.get(key);
39
+ if (!data) {
40
+ return { exists: false };
41
+ }
42
+ const session = typeof data === "string" ? JSON.parse(data) : data;
43
+ return { exists: true, session };
44
+ }
45
+ async getFeatureFlag(sessionId, flagName) {
46
+ const result = await this.getSession(sessionId);
47
+ if (!result.exists)
48
+ return null;
49
+ return result.session.featureFlags[flagName] ?? null;
50
+ }
51
+ async getAllFeatureFlags(sessionId) {
52
+ const result = await this.getSession(sessionId);
53
+ if (!result.exists)
54
+ return null;
55
+ return result.session.featureFlags;
56
+ }
57
+ }
58
+ exports.SessionService = SessionService;
@@ -0,0 +1,144 @@
1
+ import type { Redis } from "@upstash/redis";
2
+ export type DeepPartial<T> = {
3
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
4
+ };
5
+ export type FeatureFlagDefinition = {
6
+ possibleValues: string[];
7
+ defaultValue: string;
8
+ description?: string;
9
+ };
10
+ export type FeatureFlagConfig = FeatureFlagDefinition & {
11
+ name: string;
12
+ };
13
+ export type FeatureFlagDefinitions = Record<string, FeatureFlagDefinition>;
14
+ export type FeatureFlagAssignment = string | Record<string, number>;
15
+ export type SessionMetadata<TFeatureFlags extends Record<string, string> = Record<string, string>> = {
16
+ sessionId: string;
17
+ createdAt: number;
18
+ featureFlags: TFeatureFlags;
19
+ metadata?: Record<string, unknown>;
20
+ };
21
+ export type SessionResult<TFeatureFlags extends Record<string, string> = Record<string, string>> = {
22
+ exists: true;
23
+ session: SessionMetadata<TFeatureFlags>;
24
+ } | {
25
+ exists: false;
26
+ };
27
+ export type Session<TFeatureFlags extends Record<string, string> = Record<string, string>> = {
28
+ id: string;
29
+ createdAt: number;
30
+ featureFlags: TFeatureFlags;
31
+ metadata?: Record<string, unknown>;
32
+ };
33
+ export type StandardEventMap = {
34
+ pageview: {
35
+ path: string;
36
+ };
37
+ click: {
38
+ element: string;
39
+ };
40
+ error: {
41
+ message: string;
42
+ };
43
+ warning: {
44
+ message: string;
45
+ };
46
+ info: {
47
+ message: string;
48
+ };
49
+ };
50
+ export type StandardEventName = keyof StandardEventMap;
51
+ export type EventData = {
52
+ sessionCreationTimestamp: number;
53
+ sessionId: string;
54
+ /** ISO 8601 date string for date filtering */
55
+ eventTime: string;
56
+ /** Millisecond epoch for numeric aggregation (histogram) */
57
+ eventTimeMs: number;
58
+ eventName: string;
59
+ featureFlags: Record<string, string>;
60
+ sessionMetadata?: Record<string, unknown>;
61
+ properties?: Record<string, unknown>;
62
+ };
63
+ export type CaptureEventInput<TCustomEvents extends Record<string, Record<string, unknown>> = Record<string, Record<string, unknown>>> = {
64
+ [K in keyof TCustomEvents]: {
65
+ sessionId: string;
66
+ eventName: K & string;
67
+ properties: TCustomEvents[K];
68
+ };
69
+ }[keyof TCustomEvents] | {
70
+ [K in StandardEventName]: {
71
+ sessionId: string;
72
+ eventName: K;
73
+ properties: StandardEventMap[K];
74
+ };
75
+ }[StandardEventName];
76
+ export type SchemaPropertyType = "string" | "number" | "boolean";
77
+ export type SchemaProperty = {
78
+ type: SchemaPropertyType;
79
+ };
80
+ export type EventSchema = {
81
+ properties: Record<string, SchemaProperty>;
82
+ lastUpdated: number;
83
+ version: number;
84
+ };
85
+ export type LogType = "schema_update" | "feature_flag_update" | "validation_error" | "system_error" | "index_update";
86
+ export type SystemLog = {
87
+ logType: LogType;
88
+ timestamp: number;
89
+ eventName?: string;
90
+ changes?: {
91
+ before: unknown;
92
+ after: unknown;
93
+ };
94
+ metadata?: Record<string, unknown>;
95
+ };
96
+ export type SearchIndexInfo = {
97
+ currentIndex: string;
98
+ alias: string;
99
+ schema: Record<string, unknown>;
100
+ documentCount: number;
101
+ };
102
+ export type AnalyticsConfig = {
103
+ session: {
104
+ expirationMs: number;
105
+ };
106
+ featureFlags: FeatureFlagDefinitions;
107
+ events: {
108
+ customEventRetentionDays: number;
109
+ /** Maximum number of events allowed in a single batch request. Default: 20 */
110
+ maxBatchSize: number;
111
+ };
112
+ schemaValidation: {
113
+ /** Probability of validating schema on each event, between 0 and 1. Default: 1 (every event). Set lower (e.g. 0.01) to reduce Redis costs at the expense of delayed schema detection. */
114
+ checkFrequency: number;
115
+ };
116
+ logging: {
117
+ retentionDays: number;
118
+ enabledTypes: LogType[];
119
+ };
120
+ search: {
121
+ indexName: string;
122
+ autoUpdate: boolean;
123
+ rebuildOnStartup: boolean;
124
+ };
125
+ };
126
+ export type SDKOptions = {
127
+ redis: Redis;
128
+ config: AnalyticsConfig;
129
+ };
130
+ export type ServerConfig = {
131
+ redis: {
132
+ url: string;
133
+ token: string;
134
+ };
135
+ config?: DeepPartial<AnalyticsConfig>;
136
+ };
137
+ export type ClientConfig = {
138
+ /** Base URL for the analytics endpoint. Defaults to "/api/analytics" */
139
+ endpoint?: string;
140
+ /** Debounce time in ms for batching event captures. 0 = no batching. Defaults to 0. */
141
+ flushInterval?: number;
142
+ /** Maximum number of events to send in a single batch request. Default: 20. */
143
+ maxBatchSize?: number;
144
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,6 @@
1
+ export declare function generateShortId(): string;
2
+ export declare function generateSessionId(): string;
3
+ export declare function generateEventKey(): string;
4
+ export declare function generateLogKey(): string;
5
+ export declare function selectByDistribution(distribution: Record<string, number>): string;
6
+ export declare function inferPropertyType(value: unknown): "string" | "number" | "boolean";
package/dist/utils.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateShortId = generateShortId;
4
+ exports.generateSessionId = generateSessionId;
5
+ exports.generateEventKey = generateEventKey;
6
+ exports.generateLogKey = generateLogKey;
7
+ exports.selectByDistribution = selectByDistribution;
8
+ exports.inferPropertyType = inferPropertyType;
9
+ function generateShortId() {
10
+ return Math.random().toString(36).substring(2, 10);
11
+ }
12
+ function generateSessionId() {
13
+ const timestamp = Date.now();
14
+ const shortId = generateShortId();
15
+ return `session-${timestamp}-${shortId}`;
16
+ }
17
+ function generateEventKey() {
18
+ return `event-${generateShortId()}-${Date.now()}-${generateShortId()}`;
19
+ }
20
+ function generateLogKey() {
21
+ const timestamp = Date.now();
22
+ const shortId = generateShortId();
23
+ return `system-log-${timestamp}-${shortId}`;
24
+ }
25
+ function selectByDistribution(distribution) {
26
+ const entries = Object.entries(distribution);
27
+ const totalWeight = entries.reduce((sum, [, weight]) => sum + weight, 0);
28
+ let random = Math.random() * totalWeight;
29
+ for (const [value, weight] of entries) {
30
+ random -= weight;
31
+ if (random <= 0) {
32
+ return value;
33
+ }
34
+ }
35
+ // Fallback to last value
36
+ return entries[entries.length - 1][0];
37
+ }
38
+ function inferPropertyType(value) {
39
+ if (typeof value === "boolean")
40
+ return "boolean";
41
+ if (typeof value === "number")
42
+ return "number";
43
+ return "string";
44
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@upstash/redis-analytics",
3
+ "version": "0.1.0",
4
+ "main": "./dist/index.js",
5
+ "types": "./dist/index.d.ts",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./react": {
12
+ "types": "./dist/react.d.ts",
13
+ "default": "./dist/react.js"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "dev": "tsc --watch"
19
+ },
20
+ "dependencies": {
21
+ "@upstash/redis": "1.37.0-rc.12"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18 || ^19"
25
+ },
26
+ "peerDependenciesMeta": {
27
+ "react": {
28
+ "optional": true
29
+ }
30
+ },
31
+ "devDependencies": {
32
+ "@types/node": "^20",
33
+ "@types/react": "^19",
34
+ "typescript": "^5"
35
+ }
36
+ }