digital-products 2.1.3 → 2.4.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/.turbo/turbo-build.log +4 -5
- package/CHANGELOG.md +17 -0
- package/README.md +2 -0
- package/dist/api.js +7 -7
- package/dist/api.js.map +1 -1
- package/dist/app.js +6 -6
- package/dist/app.js.map +1 -1
- package/dist/client.d.ts +157 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +69 -0
- package/dist/client.js.map +1 -0
- package/dist/content.js +7 -7
- package/dist/content.js.map +1 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +6 -6
- package/dist/data.js.map +1 -1
- package/dist/dataset.js +5 -5
- package/dist/dataset.js.map +1 -1
- package/dist/index.d.ts +92 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +139 -15
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +17 -10
- package/dist/mcp.js.map +1 -1
- package/dist/product.js +2 -2
- package/dist/product.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +52 -16
- package/dist/sdk.js.map +1 -1
- package/dist/site.d.ts.map +1 -1
- package/dist/site.js +12 -8
- package/dist/site.js.map +1 -1
- package/dist/types.d.ts +830 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +495 -2
- package/dist/types.js.map +1 -1
- package/dist/worker.d.ts +205 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +356 -0
- package/dist/worker.js.map +1 -0
- package/package.json +29 -13
- package/src/api.ts +7 -7
- package/src/app.ts +6 -6
- package/src/client.ts +192 -0
- package/src/content.ts +7 -7
- package/src/data.ts +12 -7
- package/src/dataset.ts +5 -5
- package/src/index.ts +151 -15
- package/src/mcp.ts +18 -11
- package/src/product.ts +2 -2
- package/src/sdk.ts +54 -15
- package/src/site.ts +12 -8
- package/src/types.ts +821 -12
- package/src/worker.ts +525 -0
- package/test/product.test.ts +53 -198
- package/test/unified-types.test.ts +589 -0
- package/test/worker.test.ts +912 -0
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/LICENSE +0 -21
- package/dist/features/define.d.ts +0 -63
- package/dist/features/define.d.ts.map +0 -1
- package/dist/features/define.js +0 -72
- package/dist/features/define.js.map +0 -1
- package/dist/features/flags.d.ts +0 -98
- package/dist/features/flags.d.ts.map +0 -1
- package/dist/features/flags.js +0 -145
- package/dist/features/flags.js.map +0 -1
- package/dist/features/toggles.d.ts +0 -75
- package/dist/features/toggles.d.ts.map +0 -1
- package/dist/features/toggles.js +0 -107
- package/dist/features/toggles.js.map +0 -1
- package/dist/tiers/define.d.ts +0 -63
- package/dist/tiers/define.d.ts.map +0 -1
- package/dist/tiers/define.js +0 -78
- package/dist/tiers/define.js.map +0 -1
- package/dist/tiers/entitlements.d.ts +0 -94
- package/dist/tiers/entitlements.d.ts.map +0 -1
- package/dist/tiers/entitlements.js +0 -94
- package/dist/tiers/entitlements.js.map +0 -1
- package/src/api.js +0 -128
- package/src/app.js +0 -106
- package/src/content.js +0 -77
- package/src/data.js +0 -106
- package/src/dataset.js +0 -49
- package/src/entities/ai.js +0 -858
- package/src/entities/content.js +0 -783
- package/src/entities/index.js +0 -88
- package/src/entities/interfaces.js +0 -929
- package/src/entities/lifecycle.js +0 -803
- package/src/entities/products.js +0 -797
- package/src/entities/web.js +0 -657
- package/src/features/define.ts +0 -130
- package/src/features/flags.ts +0 -247
- package/src/features/toggles.ts +0 -189
- package/src/index.js +0 -35
- package/src/mcp.js +0 -139
- package/src/pricing/billing.ts +0 -386
- package/src/pricing/plans.ts +0 -214
- package/src/product.js +0 -53
- package/src/registry.js +0 -31
- package/src/sdk.js +0 -127
- package/src/site.js +0 -112
- package/src/tiers/define.ts +0 -137
- package/src/tiers/entitlements.ts +0 -201
- package/src/types.js +0 -4
- package/test/analytics/events.test.ts +0 -319
- package/test/analytics/experiments.test.ts +0 -327
- package/test/features/define.test.ts +0 -187
- package/test/features/flags.test.ts +0 -259
- package/test/features/toggles.test.ts +0 -178
- package/test/lifecycle/stages.test.ts +0 -233
- package/test/lifecycle/transitions.test.ts +0 -207
- package/test/onboarding/flows.test.ts +0 -307
- package/test/pricing/billing.test.ts +0 -287
- package/test/pricing/plans.test.ts +0 -307
- package/test/roadmap/milestones.test.ts +0 -231
- package/test/roadmap/priorities.test.ts +0 -239
- package/test/tiers/define.test.ts +0 -192
- package/test/tiers/entitlements.test.ts +0 -220
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entitlements
|
|
3
|
-
* Feature and resource entitlement management
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Entitlement types
|
|
8
|
-
*/
|
|
9
|
-
export type EntitlementType = 'boolean' | 'limit' | 'quota' | 'feature'
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Quota period
|
|
13
|
-
*/
|
|
14
|
-
export type QuotaPeriod = 'daily' | 'weekly' | 'monthly' | 'yearly'
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Entitlement definition
|
|
18
|
-
*/
|
|
19
|
-
export interface EntitlementDefinition {
|
|
20
|
-
/** Unique identifier */
|
|
21
|
-
id: string
|
|
22
|
-
/** Human-readable name */
|
|
23
|
-
name: string
|
|
24
|
-
/** Entitlement type */
|
|
25
|
-
type: EntitlementType
|
|
26
|
-
/** Description */
|
|
27
|
-
description?: string
|
|
28
|
-
/** Limit value (for limit type) */
|
|
29
|
-
limit?: number
|
|
30
|
-
/** Unit (for limit type) */
|
|
31
|
-
unit?: string
|
|
32
|
-
/** Quota value (for quota type) */
|
|
33
|
-
quota?: number
|
|
34
|
-
/** Period (for quota type) */
|
|
35
|
-
period?: QuotaPeriod
|
|
36
|
-
/** Feature ID (for feature type) */
|
|
37
|
-
featureId?: string
|
|
38
|
-
/** Tiers that have this entitlement */
|
|
39
|
-
tiers?: string[]
|
|
40
|
-
/** Tier-specific limits */
|
|
41
|
-
tierLimits?: Record<string, number>
|
|
42
|
-
/** Tier-specific quotas */
|
|
43
|
-
tierQuotas?: Record<string, number>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Create an entitlement
|
|
48
|
-
*/
|
|
49
|
-
export function Entitlement(config: EntitlementDefinition): EntitlementDefinition {
|
|
50
|
-
return {
|
|
51
|
-
id: config.id,
|
|
52
|
-
name: config.name,
|
|
53
|
-
type: config.type,
|
|
54
|
-
description: config.description,
|
|
55
|
-
limit: config.limit,
|
|
56
|
-
unit: config.unit,
|
|
57
|
-
quota: config.quota,
|
|
58
|
-
period: config.period,
|
|
59
|
-
featureId: config.featureId,
|
|
60
|
-
tiers: config.tiers,
|
|
61
|
-
tierLimits: config.tierLimits,
|
|
62
|
-
tierQuotas: config.tierQuotas,
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Entitlement check request
|
|
68
|
-
*/
|
|
69
|
-
export interface EntitlementCheck {
|
|
70
|
-
entitlementId: string
|
|
71
|
-
tier: string
|
|
72
|
-
currentUsage?: number
|
|
73
|
-
requestedUsage?: number
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Quota check result
|
|
78
|
-
*/
|
|
79
|
-
export interface QuotaCheckResult {
|
|
80
|
-
allowed: boolean
|
|
81
|
-
remaining: number
|
|
82
|
-
limit: number
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Entitlement comparison result
|
|
87
|
-
*/
|
|
88
|
-
export interface EntitlementComparisonResult {
|
|
89
|
-
added: string[]
|
|
90
|
-
removed: string[]
|
|
91
|
-
unchanged: string[]
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Entitlement manager interface
|
|
96
|
-
*/
|
|
97
|
-
export interface EntitlementManager {
|
|
98
|
-
/** Register an entitlement */
|
|
99
|
-
register(entitlement: EntitlementDefinition): void
|
|
100
|
-
/** Get an entitlement by ID */
|
|
101
|
-
get(id: string): EntitlementDefinition | undefined
|
|
102
|
-
/** Check if tier has entitlement */
|
|
103
|
-
hasEntitlement(entitlementId: string, tier: string): boolean
|
|
104
|
-
/** Get limit for tier */
|
|
105
|
-
getLimit(entitlementId: string, tier: string): number | undefined
|
|
106
|
-
/** Check quota */
|
|
107
|
-
checkQuota(check: EntitlementCheck): QuotaCheckResult
|
|
108
|
-
/** Get all entitlements for a tier */
|
|
109
|
-
getEntitlementsForTier(tier: string): EntitlementDefinition[]
|
|
110
|
-
/** Compare entitlements between tiers */
|
|
111
|
-
compareEntitlements(tierA: string, tierB: string): EntitlementComparisonResult
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Create an entitlement manager
|
|
116
|
-
*/
|
|
117
|
-
export function createEntitlementManager(): EntitlementManager {
|
|
118
|
-
const entitlements = new Map<string, EntitlementDefinition>()
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
register(entitlement: EntitlementDefinition): void {
|
|
122
|
-
entitlements.set(entitlement.id, entitlement)
|
|
123
|
-
},
|
|
124
|
-
|
|
125
|
-
get(id: string): EntitlementDefinition | undefined {
|
|
126
|
-
return entitlements.get(id)
|
|
127
|
-
},
|
|
128
|
-
|
|
129
|
-
hasEntitlement(entitlementId: string, tier: string): boolean {
|
|
130
|
-
const entitlement = entitlements.get(entitlementId)
|
|
131
|
-
if (!entitlement) return false
|
|
132
|
-
|
|
133
|
-
return entitlement.tiers?.includes(tier) ?? false
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
getLimit(entitlementId: string, tier: string): number | undefined {
|
|
137
|
-
const entitlement = entitlements.get(entitlementId)
|
|
138
|
-
if (!entitlement) return undefined
|
|
139
|
-
|
|
140
|
-
return entitlement.tierLimits?.[tier] ?? entitlement.limit
|
|
141
|
-
},
|
|
142
|
-
|
|
143
|
-
checkQuota(check: EntitlementCheck): QuotaCheckResult {
|
|
144
|
-
const entitlement = entitlements.get(check.entitlementId)
|
|
145
|
-
if (!entitlement) {
|
|
146
|
-
return { allowed: false, remaining: 0, limit: 0 }
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const limit = entitlement.tierQuotas?.[check.tier] ?? entitlement.quota ?? 0
|
|
150
|
-
const currentUsage = check.currentUsage ?? 0
|
|
151
|
-
const requestedUsage = check.requestedUsage ?? 0
|
|
152
|
-
|
|
153
|
-
// Unlimited (-1)
|
|
154
|
-
if (limit === -1) {
|
|
155
|
-
return { allowed: true, remaining: Infinity, limit: -1 }
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const remaining = Math.max(0, limit - currentUsage)
|
|
159
|
-
const allowed = currentUsage + requestedUsage <= limit
|
|
160
|
-
|
|
161
|
-
return { allowed, remaining: remaining - (allowed ? requestedUsage : 0), limit }
|
|
162
|
-
},
|
|
163
|
-
|
|
164
|
-
getEntitlementsForTier(tier: string): EntitlementDefinition[] {
|
|
165
|
-
return Array.from(entitlements.values()).filter((e) => e.tiers?.includes(tier))
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
compareEntitlements(tierA: string, tierB: string): EntitlementComparisonResult {
|
|
169
|
-
const tierAEnts = new Set(
|
|
170
|
-
Array.from(entitlements.values())
|
|
171
|
-
.filter((e) => e.tiers?.includes(tierA))
|
|
172
|
-
.map((e) => e.id)
|
|
173
|
-
)
|
|
174
|
-
const tierBEnts = new Set(
|
|
175
|
-
Array.from(entitlements.values())
|
|
176
|
-
.filter((e) => e.tiers?.includes(tierB))
|
|
177
|
-
.map((e) => e.id)
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
const added: string[] = []
|
|
181
|
-
const removed: string[] = []
|
|
182
|
-
const unchanged: string[] = []
|
|
183
|
-
|
|
184
|
-
for (const id of tierBEnts) {
|
|
185
|
-
if (tierAEnts.has(id)) {
|
|
186
|
-
unchanged.push(id)
|
|
187
|
-
} else {
|
|
188
|
-
added.push(id)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
for (const id of tierAEnts) {
|
|
193
|
-
if (!tierBEnts.has(id)) {
|
|
194
|
-
removed.push(id)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return { added, removed, unchanged }
|
|
199
|
-
},
|
|
200
|
-
}
|
|
201
|
-
}
|
package/src/types.js
DELETED
|
@@ -1,319 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Analytics Events
|
|
3
|
-
* Phase 1: RED - These tests should FAIL initially
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { describe, it, expect, beforeEach } from 'vitest'
|
|
7
|
-
import {
|
|
8
|
-
ProductEvent,
|
|
9
|
-
createProductEvent,
|
|
10
|
-
EventSchema,
|
|
11
|
-
createEventSchema,
|
|
12
|
-
EventEmitter,
|
|
13
|
-
createEventEmitter,
|
|
14
|
-
EventTracker,
|
|
15
|
-
createEventTracker,
|
|
16
|
-
} from '../../src/analytics/events.js'
|
|
17
|
-
|
|
18
|
-
describe('Analytics Events', () => {
|
|
19
|
-
describe('ProductEvent', () => {
|
|
20
|
-
it('creates a basic event', () => {
|
|
21
|
-
const event = createProductEvent({
|
|
22
|
-
name: 'page.view',
|
|
23
|
-
properties: {
|
|
24
|
-
path: '/dashboard',
|
|
25
|
-
title: 'Dashboard',
|
|
26
|
-
},
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
expect(event.name).toBe('page.view')
|
|
30
|
-
expect(event.properties.path).toBe('/dashboard')
|
|
31
|
-
expect(event.timestamp).toBeDefined()
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it('auto-generates timestamp', () => {
|
|
35
|
-
const before = Date.now()
|
|
36
|
-
const event = createProductEvent({
|
|
37
|
-
name: 'test.event',
|
|
38
|
-
})
|
|
39
|
-
const after = Date.now()
|
|
40
|
-
|
|
41
|
-
expect(event.timestamp.getTime()).toBeGreaterThanOrEqual(before)
|
|
42
|
-
expect(event.timestamp.getTime()).toBeLessThanOrEqual(after)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('supports custom timestamp', () => {
|
|
46
|
-
const customTime = new Date('2024-01-15T10:00:00Z')
|
|
47
|
-
const event = createProductEvent({
|
|
48
|
-
name: 'test.event',
|
|
49
|
-
timestamp: customTime,
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
expect(event.timestamp).toEqual(customTime)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('supports user context', () => {
|
|
56
|
-
const event = createProductEvent({
|
|
57
|
-
name: 'button.click',
|
|
58
|
-
userId: 'user-123',
|
|
59
|
-
sessionId: 'session-456',
|
|
60
|
-
properties: { button: 'submit' },
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
expect(event.userId).toBe('user-123')
|
|
64
|
-
expect(event.sessionId).toBe('session-456')
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
it('supports product context', () => {
|
|
68
|
-
const event = createProductEvent({
|
|
69
|
-
name: 'feature.used',
|
|
70
|
-
productId: 'my-product',
|
|
71
|
-
version: '2.0.0',
|
|
72
|
-
tier: 'pro',
|
|
73
|
-
properties: { feature: 'analytics' },
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
expect(event.productId).toBe('my-product')
|
|
77
|
-
expect(event.version).toBe('2.0.0')
|
|
78
|
-
expect(event.tier).toBe('pro')
|
|
79
|
-
})
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
describe('EventSchema', () => {
|
|
83
|
-
it('defines required properties', () => {
|
|
84
|
-
const schema = createEventSchema({
|
|
85
|
-
name: 'signup.completed',
|
|
86
|
-
properties: {
|
|
87
|
-
email: { type: 'string', required: true },
|
|
88
|
-
plan: { type: 'string', required: true },
|
|
89
|
-
source: { type: 'string', required: false },
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
expect(schema.properties.email.required).toBe(true)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('validates event against schema', () => {
|
|
97
|
-
const schema = createEventSchema({
|
|
98
|
-
name: 'purchase.completed',
|
|
99
|
-
properties: {
|
|
100
|
-
amount: { type: 'number', required: true },
|
|
101
|
-
currency: { type: 'string', required: true },
|
|
102
|
-
},
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const validEvent = createProductEvent({
|
|
106
|
-
name: 'purchase.completed',
|
|
107
|
-
properties: { amount: 99.99, currency: 'USD' },
|
|
108
|
-
})
|
|
109
|
-
|
|
110
|
-
const invalidEvent = createProductEvent({
|
|
111
|
-
name: 'purchase.completed',
|
|
112
|
-
properties: { amount: 99.99 }, // Missing currency
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
expect(schema.validate(validEvent)).toEqual({ valid: true })
|
|
116
|
-
expect(schema.validate(invalidEvent)).toEqual({
|
|
117
|
-
valid: false,
|
|
118
|
-
errors: ['Missing required property: currency'],
|
|
119
|
-
})
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
it('validates property types', () => {
|
|
123
|
-
const schema = createEventSchema({
|
|
124
|
-
name: 'test.event',
|
|
125
|
-
properties: {
|
|
126
|
-
count: { type: 'number', required: true },
|
|
127
|
-
},
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
const invalidEvent = createProductEvent({
|
|
131
|
-
name: 'test.event',
|
|
132
|
-
properties: { count: 'not-a-number' },
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
expect(schema.validate(invalidEvent)).toEqual({
|
|
136
|
-
valid: false,
|
|
137
|
-
errors: ['Property count should be number, got string'],
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
it('supports enum values', () => {
|
|
142
|
-
const schema = createEventSchema({
|
|
143
|
-
name: 'plan.selected',
|
|
144
|
-
properties: {
|
|
145
|
-
plan: {
|
|
146
|
-
type: 'string',
|
|
147
|
-
required: true,
|
|
148
|
-
enum: ['free', 'pro', 'enterprise'],
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
const valid = createProductEvent({
|
|
154
|
-
name: 'plan.selected',
|
|
155
|
-
properties: { plan: 'pro' },
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
const invalid = createProductEvent({
|
|
159
|
-
name: 'plan.selected',
|
|
160
|
-
properties: { plan: 'invalid-plan' },
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
expect(schema.validate(valid).valid).toBe(true)
|
|
164
|
-
expect(schema.validate(invalid).valid).toBe(false)
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
describe('EventEmitter', () => {
|
|
169
|
-
let emitter: EventEmitter
|
|
170
|
-
|
|
171
|
-
beforeEach(() => {
|
|
172
|
-
emitter = createEventEmitter()
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
it('emits events', () => {
|
|
176
|
-
const events: ProductEvent[] = []
|
|
177
|
-
emitter.on('page.view', (e) => events.push(e))
|
|
178
|
-
|
|
179
|
-
emitter.emit(
|
|
180
|
-
createProductEvent({
|
|
181
|
-
name: 'page.view',
|
|
182
|
-
properties: { path: '/' },
|
|
183
|
-
})
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
expect(events).toHaveLength(1)
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('supports wildcard listeners', () => {
|
|
190
|
-
const events: ProductEvent[] = []
|
|
191
|
-
emitter.on('*', (e) => events.push(e))
|
|
192
|
-
|
|
193
|
-
emitter.emit(createProductEvent({ name: 'event.one' }))
|
|
194
|
-
emitter.emit(createProductEvent({ name: 'event.two' }))
|
|
195
|
-
|
|
196
|
-
expect(events).toHaveLength(2)
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
it('supports pattern matching', () => {
|
|
200
|
-
const events: ProductEvent[] = []
|
|
201
|
-
emitter.on('user.*', (e) => events.push(e))
|
|
202
|
-
|
|
203
|
-
emitter.emit(createProductEvent({ name: 'user.signup' }))
|
|
204
|
-
emitter.emit(createProductEvent({ name: 'user.login' }))
|
|
205
|
-
emitter.emit(createProductEvent({ name: 'page.view' }))
|
|
206
|
-
|
|
207
|
-
expect(events).toHaveLength(2)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('removes listeners', () => {
|
|
211
|
-
const events: ProductEvent[] = []
|
|
212
|
-
const handler = (e: ProductEvent) => events.push(e)
|
|
213
|
-
|
|
214
|
-
emitter.on('test', handler)
|
|
215
|
-
emitter.emit(createProductEvent({ name: 'test' }))
|
|
216
|
-
expect(events).toHaveLength(1)
|
|
217
|
-
|
|
218
|
-
emitter.off('test', handler)
|
|
219
|
-
emitter.emit(createProductEvent({ name: 'test' }))
|
|
220
|
-
expect(events).toHaveLength(1)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('supports once listeners', () => {
|
|
224
|
-
const events: ProductEvent[] = []
|
|
225
|
-
emitter.once('once-event', (e) => events.push(e))
|
|
226
|
-
|
|
227
|
-
emitter.emit(createProductEvent({ name: 'once-event' }))
|
|
228
|
-
emitter.emit(createProductEvent({ name: 'once-event' }))
|
|
229
|
-
|
|
230
|
-
expect(events).toHaveLength(1)
|
|
231
|
-
})
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
describe('EventTracker', () => {
|
|
235
|
-
let tracker: EventTracker
|
|
236
|
-
|
|
237
|
-
beforeEach(() => {
|
|
238
|
-
tracker = createEventTracker({
|
|
239
|
-
productId: 'test-product',
|
|
240
|
-
version: '1.0.0',
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('tracks events with product context', () => {
|
|
245
|
-
const tracked = tracker.track('button.click', { button: 'submit' })
|
|
246
|
-
|
|
247
|
-
expect(tracked.productId).toBe('test-product')
|
|
248
|
-
expect(tracked.version).toBe('1.0.0')
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('batches events', async () => {
|
|
252
|
-
const sentBatches: ProductEvent[][] = []
|
|
253
|
-
|
|
254
|
-
tracker = createEventTracker({
|
|
255
|
-
productId: 'batch-test',
|
|
256
|
-
batchSize: 3,
|
|
257
|
-
onFlush: (events) => sentBatches.push(events),
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
tracker.track('event1')
|
|
261
|
-
tracker.track('event2')
|
|
262
|
-
expect(sentBatches).toHaveLength(0)
|
|
263
|
-
|
|
264
|
-
tracker.track('event3')
|
|
265
|
-
expect(sentBatches).toHaveLength(1)
|
|
266
|
-
expect(sentBatches[0]).toHaveLength(3)
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
it('flushes on interval', async () => {
|
|
270
|
-
const sentBatches: ProductEvent[][] = []
|
|
271
|
-
|
|
272
|
-
tracker = createEventTracker({
|
|
273
|
-
productId: 'interval-test',
|
|
274
|
-
flushInterval: 50,
|
|
275
|
-
onFlush: (events) => sentBatches.push(events),
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
tracker.track('event1')
|
|
279
|
-
await new Promise((r) => setTimeout(r, 100))
|
|
280
|
-
|
|
281
|
-
expect(sentBatches).toHaveLength(1)
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
it('enriches events with global context', () => {
|
|
285
|
-
tracker.setContext({
|
|
286
|
-
userId: 'global-user',
|
|
287
|
-
tier: 'enterprise',
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
const event = tracker.track('test.event')
|
|
291
|
-
expect(event.userId).toBe('global-user')
|
|
292
|
-
expect(event.tier).toBe('enterprise')
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('validates events against schemas', () => {
|
|
296
|
-
tracker.registerSchema(
|
|
297
|
-
createEventSchema({
|
|
298
|
-
name: 'validated.event',
|
|
299
|
-
properties: {
|
|
300
|
-
required_prop: { type: 'string', required: true },
|
|
301
|
-
},
|
|
302
|
-
})
|
|
303
|
-
)
|
|
304
|
-
|
|
305
|
-
expect(() => tracker.track('validated.event', {})).toThrow(
|
|
306
|
-
'Event validation failed'
|
|
307
|
-
)
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
it('provides analytics helpers', () => {
|
|
311
|
-
tracker.pageView('/dashboard', { title: 'Dashboard' })
|
|
312
|
-
tracker.identify('user-123', { email: 'user@example.com' })
|
|
313
|
-
tracker.track('feature.used', { feature: 'analytics' })
|
|
314
|
-
|
|
315
|
-
const events = tracker.getBuffer()
|
|
316
|
-
expect(events).toHaveLength(3)
|
|
317
|
-
})
|
|
318
|
-
})
|
|
319
|
-
})
|