@stacksjs/ts-analytics 0.1.6
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/CHANGELOG.md +68 -0
- package/LICENSE.md +21 -0
- package/README.md +361 -0
- package/bin/cli.ts +169 -0
- package/dist/Analytics.d.ts +558 -0
- package/dist/api.d.ts +109 -0
- package/dist/batching.d.ts +93 -0
- package/dist/chunk-2mx7fq49.js +4 -0
- package/dist/chunk-3z29508k.js +204 -0
- package/dist/chunk-deephkz6.js +56 -0
- package/dist/chunk-j261vgyp.js +305 -0
- package/dist/chunk-xga17tz7.js +207 -0
- package/dist/config.d.ts +132 -0
- package/dist/dashboard/components/index.d.ts +80 -0
- package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
- package/dist/dashboard/index.d.ts +110 -0
- package/dist/dashboard/types/index.d.ts +144 -0
- package/dist/dashboard/utils/index.d.ts +81 -0
- package/dist/dynamodb.d.ts +86 -0
- package/dist/funnels.d.ts +104 -0
- package/dist/geolocation.d.ts +96 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +10211 -0
- package/dist/infrastructure/cdk.d.ts +60 -0
- package/dist/infrastructure/cloudformation.d.ts +44 -0
- package/dist/infrastructure/index.d.ts +45 -0
- package/dist/infrastructure/setup.d.ts +133 -0
- package/dist/integrations/cloudflare.d.ts +76 -0
- package/dist/integrations/hono.d.ts +60 -0
- package/dist/integrations/index.d.ts +24 -0
- package/dist/integrations/nuxt.d.ts +7 -0
- package/dist/integrations/nuxt.js +42 -0
- package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
- package/dist/integrations/runtime/use-ts-analytics.js +17 -0
- package/dist/integrations/stx.d.ts +88 -0
- package/dist/integrations/stx.js +18 -0
- package/dist/lib/crypto-random.d.ts +4 -0
- package/dist/lib/salt.d.ts +16 -0
- package/dist/local.d.ts +56 -0
- package/dist/model-connector.d.ts +145 -0
- package/dist/models/AggregatedStats.d.ts +9 -0
- package/dist/models/CampaignStats.d.ts +9 -0
- package/dist/models/Conversion.d.ts +11 -0
- package/dist/models/CustomEvent.d.ts +11 -0
- package/dist/models/DeviceStats.d.ts +9 -0
- package/dist/models/EventStats.d.ts +9 -0
- package/dist/models/GeoStats.d.ts +9 -0
- package/dist/models/Goal.d.ts +9 -0
- package/dist/models/GoalStats.d.ts +9 -0
- package/dist/models/PageStats.d.ts +11 -0
- package/dist/models/PageView.d.ts +13 -0
- package/dist/models/RealtimeStats.d.ts +9 -0
- package/dist/models/ReferrerStats.d.ts +9 -0
- package/dist/models/Session.d.ts +11 -0
- package/dist/models/Site.d.ts +11 -0
- package/dist/models/index.d.ts +28 -0
- package/dist/models/types.d.ts +60 -0
- package/dist/sqs-buffering.d.ts +243 -0
- package/dist/stacks-integration.d.ts +159 -0
- package/dist/tracking-script.d.ts +71 -0
- package/dist/tracking.d.ts +17 -0
- package/dist/tracking.js +26 -0
- package/dist/types.d.ts +595 -0
- package/dist/utils/geolocation.d.ts +111 -0
- package/dist/utils/user-agent.d.ts +17 -0
- package/dist/version.d.ts +7 -0
- package/package.json +119 -0
- package/src/Analytics.ts +3349 -0
- package/src/api.ts +1286 -0
- package/src/assets/crosswind.css +2220 -0
- package/src/batching.ts +452 -0
- package/src/components/dashboard/index.ts +18 -0
- package/src/config.ts +456 -0
- package/src/dashboard/Dashboard.stx +1517 -0
- package/src/dashboard/components/AlertCard.stx +177 -0
- package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
- package/src/dashboard/components/AnimatedNumber.stx +86 -0
- package/src/dashboard/components/BarChart.stx +220 -0
- package/src/dashboard/components/BrowserBreakdown.stx +98 -0
- package/src/dashboard/components/BrowsersTable.stx +125 -0
- package/src/dashboard/components/CampaignBreakdown.stx +163 -0
- package/src/dashboard/components/CampaignTable.stx +238 -0
- package/src/dashboard/components/CountryList.stx +101 -0
- package/src/dashboard/components/DataTable.stx +226 -0
- package/src/dashboard/components/DateRangePicker.stx +77 -0
- package/src/dashboard/components/DeviceBreakdown.stx +94 -0
- package/src/dashboard/components/DevicesTable.stx +163 -0
- package/src/dashboard/components/DonutChart.stories.ts +55 -0
- package/src/dashboard/components/DonutChart.stx +157 -0
- package/src/dashboard/components/EmptyState.stx +90 -0
- package/src/dashboard/components/EngagementMetrics.stx +183 -0
- package/src/dashboard/components/EventsSection.stx +192 -0
- package/src/dashboard/components/FilterBar.stx +104 -0
- package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
- package/src/dashboard/components/FunnelChart.stx +142 -0
- package/src/dashboard/components/GeoTable.stx +337 -0
- package/src/dashboard/components/GoalsPanel.stx +109 -0
- package/src/dashboard/components/Header.stx +39 -0
- package/src/dashboard/components/HeatmapChart.stx +140 -0
- package/src/dashboard/components/LiveActivityFeed.stx +172 -0
- package/src/dashboard/components/MetricComparison.stx +106 -0
- package/src/dashboard/components/MiniStats.stx +96 -0
- package/src/dashboard/components/OSBreakdown.stx +124 -0
- package/src/dashboard/components/PageDetailCard.stx +102 -0
- package/src/dashboard/components/PagesTable.stx +127 -0
- package/src/dashboard/components/ProgressRing.stx +89 -0
- package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
- package/src/dashboard/components/RealtimeCounter.stx +46 -0
- package/src/dashboard/components/ReferrersTable.stx +180 -0
- package/src/dashboard/components/SparklineChart.stx +160 -0
- package/src/dashboard/components/StatCard.stories.ts +58 -0
- package/src/dashboard/components/StatCard.stx +90 -0
- package/src/dashboard/components/SummaryStats.stx +81 -0
- package/src/dashboard/components/TabNav.stx +66 -0
- package/src/dashboard/components/ThemeSwitcher.stx +124 -0
- package/src/dashboard/components/TimeSeriesChart.stx +106 -0
- package/src/dashboard/components/TopList.stories.ts +58 -0
- package/src/dashboard/components/TopList.stx +74 -0
- package/src/dashboard/components/TrendIndicator.stx +84 -0
- package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
- package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
- package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
- package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
- package/src/dashboard/components/heatmap/index.ts +12 -0
- package/src/dashboard/components/index.ts +86 -0
- package/src/dashboard/composables/useAnalytics.ts +465 -0
- package/src/dashboard/demo/DemoApp.stx +370 -0
- package/src/dashboard/demo/index.ts +8 -0
- package/src/dashboard/demo/mockData.ts +234 -0
- package/src/dashboard/index.ts +117 -0
- package/src/dashboard/stx-shim.d.ts +4 -0
- package/src/dashboard/types/index.ts +203 -0
- package/src/dashboard/utils/index.ts +426 -0
- package/src/dynamodb.ts +344 -0
- package/src/funnels.ts +534 -0
- package/src/geolocation.ts +515 -0
- package/src/handlers/alerts.ts +328 -0
- package/src/handlers/annotations.ts +128 -0
- package/src/handlers/api-keys.ts +240 -0
- package/src/handlers/auth.ts +1020 -0
- package/src/handlers/authz.ts +137 -0
- package/src/handlers/collect.ts +724 -0
- package/src/handlers/data.ts +625 -0
- package/src/handlers/experiments.ts +138 -0
- package/src/handlers/funnels.ts +216 -0
- package/src/handlers/goals.ts +218 -0
- package/src/handlers/heatmaps.ts +272 -0
- package/src/handlers/index.ts +56 -0
- package/src/handlers/lib/read-cache.ts +63 -0
- package/src/handlers/misc.ts +486 -0
- package/src/handlers/oauth.ts +233 -0
- package/src/handlers/performance.ts +388 -0
- package/src/handlers/sessions.ts +413 -0
- package/src/handlers/sharing.ts +198 -0
- package/src/handlers/stats.ts +1368 -0
- package/src/handlers/team.ts +161 -0
- package/src/handlers/uptime.ts +283 -0
- package/src/handlers/views.ts +390 -0
- package/src/handlers/webhooks.ts +226 -0
- package/src/heatmap/index.ts +32 -0
- package/src/heatmap/tracking-script.ts +452 -0
- package/src/heatmap/types.ts +79 -0
- package/src/index.ts +387 -0
- package/src/infrastructure/cdk.ts +496 -0
- package/src/infrastructure/cloudformation.ts +595 -0
- package/src/infrastructure/index.ts +48 -0
- package/src/infrastructure/setup.ts +611 -0
- package/src/integrations/cloudflare.ts +732 -0
- package/src/integrations/hono.ts +589 -0
- package/src/integrations/index.ts +27 -0
- package/src/integrations/nuxt.ts +78 -0
- package/src/integrations/runtime/use-ts-analytics.ts +32 -0
- package/src/integrations/stx.ts +138 -0
- package/src/jobs/index.ts +127 -0
- package/src/lib/crypto-random.ts +41 -0
- package/src/lib/ddb-errors.ts +20 -0
- package/src/lib/dynamodb.ts +216 -0
- package/src/lib/email.ts +38 -0
- package/src/lib/ga-import.ts +471 -0
- package/src/lib/ga4-api.ts +244 -0
- package/src/lib/goals.ts +205 -0
- package/src/lib/index.ts +6 -0
- package/src/lib/ingest-counters.ts +123 -0
- package/src/lib/log.ts +36 -0
- package/src/lib/membership.ts +98 -0
- package/src/lib/plans.ts +90 -0
- package/src/lib/quota.ts +46 -0
- package/src/lib/rate-limit.ts +27 -0
- package/src/lib/rollups.ts +472 -0
- package/src/lib/salt.ts +97 -0
- package/src/lib/scheduler.ts +97 -0
- package/src/lib/significance.ts +66 -0
- package/src/lib/site-retention.ts +56 -0
- package/src/local.ts +360 -0
- package/src/model-connector.ts +616 -0
- package/src/models/AggregatedStats.ts +162 -0
- package/src/models/CampaignStats.ts +141 -0
- package/src/models/Conversion.ts +135 -0
- package/src/models/CustomEvent.ts +123 -0
- package/src/models/DeviceStats.ts +105 -0
- package/src/models/EventStats.ts +108 -0
- package/src/models/GeoStats.ts +112 -0
- package/src/models/Goal.ts +105 -0
- package/src/models/GoalStats.ts +106 -0
- package/src/models/PageStats.ts +152 -0
- package/src/models/PageView.ts +277 -0
- package/src/models/RealtimeStats.ts +96 -0
- package/src/models/ReferrerStats.ts +115 -0
- package/src/models/Session.ts +235 -0
- package/src/models/Site.ts +115 -0
- package/src/models/index.ts +50 -0
- package/src/models/orm/index.ts +2043 -0
- package/src/models/types.ts +71 -0
- package/src/router.ts +521 -0
- package/src/sqs-buffering.ts +806 -0
- package/src/stacks-integration.ts +527 -0
- package/src/tracking-script.ts +944 -0
- package/src/tracking.ts +27 -0
- package/src/types/analytics.ts +219 -0
- package/src/types/api.ts +75 -0
- package/src/types/bun-router.d.ts +15 -0
- package/src/types/dashboard.ts +62 -0
- package/src/types/index.ts +66 -0
- package/src/types/stx.d.ts +27 -0
- package/src/types/window.d.ts +101 -0
- package/src/types.ts +911 -0
- package/src/utils/cache.ts +148 -0
- package/src/utils/date.ts +118 -0
- package/src/utils/filters.ts +71 -0
- package/src/utils/geolocation.ts +323 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/response.ts +180 -0
- package/src/utils/timezone-country.ts +242 -0
- package/src/utils/user-agent.ts +110 -0
- package/src/version.ts +7 -0
package/src/funnels.ts
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funnel Analysis Utilities
|
|
3
|
+
*
|
|
4
|
+
* Tools for analyzing conversion funnels and user journeys.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CustomEvent, PageView, Session } from './types'
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface FunnelStep {
|
|
14
|
+
/** Step identifier */
|
|
15
|
+
id: string
|
|
16
|
+
/** Human-readable name */
|
|
17
|
+
name: string
|
|
18
|
+
/** Match criteria for this step */
|
|
19
|
+
match: FunnelStepMatcher
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface FunnelStepMatcher {
|
|
23
|
+
/** Path pattern (supports * wildcard) */
|
|
24
|
+
path?: string
|
|
25
|
+
/** Event name to match */
|
|
26
|
+
eventName?: string
|
|
27
|
+
/** Event category to match */
|
|
28
|
+
eventCategory?: string
|
|
29
|
+
/** Custom matcher function */
|
|
30
|
+
custom?: (event: PageView | CustomEvent) => boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Funnel {
|
|
34
|
+
/** Funnel identifier */
|
|
35
|
+
id: string
|
|
36
|
+
/** Funnel name */
|
|
37
|
+
name: string
|
|
38
|
+
/** Ordered steps in the funnel */
|
|
39
|
+
steps: FunnelStep[]
|
|
40
|
+
/** Time window in milliseconds for completing funnel */
|
|
41
|
+
windowMs?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FunnelAnalysis {
|
|
45
|
+
/** Funnel being analyzed */
|
|
46
|
+
funnel: Funnel
|
|
47
|
+
/** Date range of analysis */
|
|
48
|
+
dateRange: {
|
|
49
|
+
start: Date
|
|
50
|
+
end: Date
|
|
51
|
+
}
|
|
52
|
+
/** Total users who entered the funnel */
|
|
53
|
+
totalEntries: number
|
|
54
|
+
/** Users who completed the funnel */
|
|
55
|
+
completions: number
|
|
56
|
+
/** Overall conversion rate */
|
|
57
|
+
conversionRate: number
|
|
58
|
+
/** Step-by-step breakdown */
|
|
59
|
+
steps: FunnelStepAnalysis[]
|
|
60
|
+
/** Average time to complete (ms) */
|
|
61
|
+
avgCompletionTimeMs?: number
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FunnelStepAnalysis {
|
|
65
|
+
/** Step being analyzed */
|
|
66
|
+
step: FunnelStep
|
|
67
|
+
/** Users who reached this step */
|
|
68
|
+
reached: number
|
|
69
|
+
/** Users who dropped off at this step */
|
|
70
|
+
droppedOff: number
|
|
71
|
+
/** Conversion rate from previous step */
|
|
72
|
+
conversionFromPrevious: number
|
|
73
|
+
/** Overall conversion rate from funnel start */
|
|
74
|
+
conversionFromStart: number
|
|
75
|
+
/** Average time spent on this step (ms) */
|
|
76
|
+
avgTimeOnStepMs?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface UserJourney {
|
|
80
|
+
/** Visitor ID */
|
|
81
|
+
visitorId: string
|
|
82
|
+
/** Session ID */
|
|
83
|
+
sessionId: string
|
|
84
|
+
/** Steps completed */
|
|
85
|
+
stepsCompleted: number
|
|
86
|
+
/** Total steps in funnel */
|
|
87
|
+
totalSteps: number
|
|
88
|
+
/** Whether funnel was completed */
|
|
89
|
+
completed: boolean
|
|
90
|
+
/** Timestamps for each step */
|
|
91
|
+
stepTimestamps: Date[]
|
|
92
|
+
/** Time to complete (ms) */
|
|
93
|
+
completionTimeMs?: number
|
|
94
|
+
/** Drop-off step (if not completed) */
|
|
95
|
+
dropOffStep?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ============================================================================
|
|
99
|
+
// Funnel Builder
|
|
100
|
+
// ============================================================================
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Fluent builder for creating funnels
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const checkout = createFunnel('checkout')
|
|
108
|
+
* .name('Checkout Flow')
|
|
109
|
+
* .step('view_product', 'View Product', { path: '/products/*' })
|
|
110
|
+
* .step('add_to_cart', 'Add to Cart', { eventName: 'add_to_cart' })
|
|
111
|
+
* .step('checkout', 'Checkout', { path: '/checkout' })
|
|
112
|
+
* .step('purchase', 'Purchase', { eventName: 'purchase' })
|
|
113
|
+
* .window(30 * 60 * 1000) // 30 minutes
|
|
114
|
+
* .build()
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export function createFunnel(id: string): FunnelBuilder {
|
|
118
|
+
return new FunnelBuilder(id)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
class FunnelBuilder {
|
|
122
|
+
private funnel: Funnel
|
|
123
|
+
|
|
124
|
+
constructor(id: string) {
|
|
125
|
+
this.funnel = {
|
|
126
|
+
id,
|
|
127
|
+
name: id,
|
|
128
|
+
steps: [],
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
name(name: string): this {
|
|
133
|
+
this.funnel.name = name
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
step(id: string, name: string, match: FunnelStepMatcher): this {
|
|
138
|
+
this.funnel.steps.push({ id, name, match })
|
|
139
|
+
return this
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
window(windowMs: number): this {
|
|
143
|
+
this.funnel.windowMs = windowMs
|
|
144
|
+
return this
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
build(): Funnel {
|
|
148
|
+
if (this.funnel.steps.length < 2) {
|
|
149
|
+
throw new Error('Funnel must have at least 2 steps')
|
|
150
|
+
}
|
|
151
|
+
return { ...this.funnel }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Funnel Analyzer
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Analyze funnel performance from event data
|
|
161
|
+
*/
|
|
162
|
+
export class FunnelAnalyzer {
|
|
163
|
+
private funnel: Funnel
|
|
164
|
+
|
|
165
|
+
constructor(funnel: Funnel) {
|
|
166
|
+
this.funnel = funnel
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Analyze funnel performance from page views and events
|
|
171
|
+
*/
|
|
172
|
+
analyze(
|
|
173
|
+
pageViews: PageView[],
|
|
174
|
+
events: CustomEvent[],
|
|
175
|
+
sessions: Session[],
|
|
176
|
+
): FunnelAnalysis {
|
|
177
|
+
// Group events by session
|
|
178
|
+
const sessionEvents = this.groupBySession(pageViews, events)
|
|
179
|
+
|
|
180
|
+
// Analyze each session's journey through the funnel
|
|
181
|
+
const journeys = this.analyzeJourneys(sessionEvents)
|
|
182
|
+
|
|
183
|
+
// Calculate metrics
|
|
184
|
+
const totalEntries = journeys.filter(j => j.stepsCompleted >= 1).length
|
|
185
|
+
const completions = journeys.filter(j => j.completed).length
|
|
186
|
+
const conversionRate = totalEntries > 0 ? completions / totalEntries : 0
|
|
187
|
+
|
|
188
|
+
// Calculate completion times
|
|
189
|
+
const completionTimes = journeys
|
|
190
|
+
.filter(j => j.completed && j.completionTimeMs)
|
|
191
|
+
.map(j => j.completionTimeMs!)
|
|
192
|
+
const avgCompletionTimeMs = completionTimes.length > 0
|
|
193
|
+
? completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length
|
|
194
|
+
: undefined
|
|
195
|
+
|
|
196
|
+
// Analyze each step
|
|
197
|
+
const steps = this.analyzeSteps(journeys, totalEntries)
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
funnel: this.funnel,
|
|
201
|
+
dateRange: this.getDateRange(pageViews, events),
|
|
202
|
+
totalEntries,
|
|
203
|
+
completions,
|
|
204
|
+
conversionRate,
|
|
205
|
+
steps,
|
|
206
|
+
avgCompletionTimeMs,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get individual user journeys through the funnel
|
|
212
|
+
*/
|
|
213
|
+
getJourneys(
|
|
214
|
+
pageViews: PageView[],
|
|
215
|
+
events: CustomEvent[],
|
|
216
|
+
): UserJourney[] {
|
|
217
|
+
const sessionEvents = this.groupBySession(pageViews, events)
|
|
218
|
+
return this.analyzeJourneys(sessionEvents)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private groupBySession(
|
|
222
|
+
pageViews: PageView[],
|
|
223
|
+
events: CustomEvent[],
|
|
224
|
+
): Map<string, Array<PageView | CustomEvent>> {
|
|
225
|
+
const sessionEvents = new Map<string, Array<PageView | CustomEvent>>()
|
|
226
|
+
|
|
227
|
+
// Add page views
|
|
228
|
+
for (const pv of pageViews) {
|
|
229
|
+
const key = `${pv.visitorId}:${pv.sessionId}`
|
|
230
|
+
if (!sessionEvents.has(key)) {
|
|
231
|
+
sessionEvents.set(key, [])
|
|
232
|
+
}
|
|
233
|
+
sessionEvents.get(key)!.push(pv)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Add custom events
|
|
237
|
+
for (const event of events) {
|
|
238
|
+
const key = `${event.visitorId}:${event.sessionId}`
|
|
239
|
+
if (!sessionEvents.has(key)) {
|
|
240
|
+
sessionEvents.set(key, [])
|
|
241
|
+
}
|
|
242
|
+
sessionEvents.get(key)!.push(event)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Sort by timestamp
|
|
246
|
+
for (const [key, eventList] of sessionEvents) {
|
|
247
|
+
eventList.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return sessionEvents
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private analyzeJourneys(
|
|
254
|
+
sessionEvents: Map<string, Array<PageView | CustomEvent>>,
|
|
255
|
+
): UserJourney[] {
|
|
256
|
+
const journeys: UserJourney[] = []
|
|
257
|
+
|
|
258
|
+
for (const [key, eventList] of sessionEvents) {
|
|
259
|
+
const [visitorId, sessionId] = key.split(':')
|
|
260
|
+
const journey = this.analyzeSessionJourney(visitorId, sessionId, eventList)
|
|
261
|
+
if (journey.stepsCompleted > 0) {
|
|
262
|
+
journeys.push(journey)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return journeys
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private analyzeSessionJourney(
|
|
270
|
+
visitorId: string,
|
|
271
|
+
sessionId: string,
|
|
272
|
+
events: Array<PageView | CustomEvent>,
|
|
273
|
+
): UserJourney {
|
|
274
|
+
const stepTimestamps: Date[] = []
|
|
275
|
+
let currentStepIndex = 0
|
|
276
|
+
let funnelStartTime: Date | undefined
|
|
277
|
+
|
|
278
|
+
for (const event of events) {
|
|
279
|
+
// Check time window
|
|
280
|
+
if (funnelStartTime && this.funnel.windowMs) {
|
|
281
|
+
const elapsed = event.timestamp.getTime() - funnelStartTime.getTime()
|
|
282
|
+
if (elapsed > this.funnel.windowMs) {
|
|
283
|
+
break // Time window exceeded
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Check if event matches current step
|
|
288
|
+
const currentStep = this.funnel.steps[currentStepIndex]
|
|
289
|
+
if (currentStep && this.matchesStep(event, currentStep)) {
|
|
290
|
+
stepTimestamps.push(event.timestamp)
|
|
291
|
+
|
|
292
|
+
if (currentStepIndex === 0) {
|
|
293
|
+
funnelStartTime = event.timestamp
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
currentStepIndex++
|
|
297
|
+
|
|
298
|
+
// Check if funnel completed
|
|
299
|
+
if (currentStepIndex >= this.funnel.steps.length) {
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const completed = currentStepIndex >= this.funnel.steps.length
|
|
306
|
+
const completionTimeMs = completed && stepTimestamps.length >= 2
|
|
307
|
+
? stepTimestamps[stepTimestamps.length - 1].getTime() - stepTimestamps[0].getTime()
|
|
308
|
+
: undefined
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
visitorId,
|
|
312
|
+
sessionId,
|
|
313
|
+
stepsCompleted: currentStepIndex,
|
|
314
|
+
totalSteps: this.funnel.steps.length,
|
|
315
|
+
completed,
|
|
316
|
+
stepTimestamps,
|
|
317
|
+
completionTimeMs,
|
|
318
|
+
dropOffStep: !completed && currentStepIndex < this.funnel.steps.length
|
|
319
|
+
? this.funnel.steps[currentStepIndex].id
|
|
320
|
+
: undefined,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private matchesStep(event: PageView | CustomEvent, step: FunnelStep): boolean {
|
|
325
|
+
const { match } = step
|
|
326
|
+
|
|
327
|
+
// Custom matcher takes precedence
|
|
328
|
+
if (match.custom) {
|
|
329
|
+
return match.custom(event)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Path matching
|
|
333
|
+
if (match.path) {
|
|
334
|
+
const path = 'path' in event ? event.path : undefined
|
|
335
|
+
if (path && !this.matchPath(path, match.path)) {
|
|
336
|
+
return false
|
|
337
|
+
}
|
|
338
|
+
if (match.path && !path) {
|
|
339
|
+
return false
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Event name matching
|
|
344
|
+
if (match.eventName) {
|
|
345
|
+
const eventName = 'name' in event ? event.name : undefined
|
|
346
|
+
if (eventName !== match.eventName) {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Event category matching
|
|
352
|
+
if (match.eventCategory) {
|
|
353
|
+
const category = 'category' in event ? event.category : undefined
|
|
354
|
+
if (category !== match.eventCategory) {
|
|
355
|
+
return false
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private matchPath(path: string, pattern: string): boolean {
|
|
363
|
+
// Convert pattern to regex
|
|
364
|
+
const regexPattern = pattern
|
|
365
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
366
|
+
.replace(/\*/g, '.*') // Convert * to .*
|
|
367
|
+
|
|
368
|
+
const regex = new RegExp(`^${regexPattern}$`)
|
|
369
|
+
return regex.test(path)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private analyzeSteps(journeys: UserJourney[], totalEntries: number): FunnelStepAnalysis[] {
|
|
373
|
+
return this.funnel.steps.map((step, index) => {
|
|
374
|
+
const reached = journeys.filter(j => j.stepsCompleted > index).length
|
|
375
|
+
const droppedOff = index === 0
|
|
376
|
+
? totalEntries - reached
|
|
377
|
+
: journeys.filter(j => j.stepsCompleted === index).length
|
|
378
|
+
|
|
379
|
+
const previousReached = index === 0 ? totalEntries : journeys.filter(j => j.stepsCompleted >= index).length
|
|
380
|
+
const conversionFromPrevious = previousReached > 0 ? reached / previousReached : 0
|
|
381
|
+
const conversionFromStart = totalEntries > 0 ? reached / totalEntries : 0
|
|
382
|
+
|
|
383
|
+
// Calculate average time on step
|
|
384
|
+
const stepTimes = journeys
|
|
385
|
+
.filter(j => j.stepsCompleted > index && j.stepTimestamps.length > index + 1)
|
|
386
|
+
.map(j => {
|
|
387
|
+
const stepStart = j.stepTimestamps[index]
|
|
388
|
+
const stepEnd = j.stepTimestamps[index + 1]
|
|
389
|
+
return stepEnd.getTime() - stepStart.getTime()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
const avgTimeOnStepMs = stepTimes.length > 0
|
|
393
|
+
? stepTimes.reduce((a, b) => a + b, 0) / stepTimes.length
|
|
394
|
+
: undefined
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
step,
|
|
398
|
+
reached,
|
|
399
|
+
droppedOff,
|
|
400
|
+
conversionFromPrevious,
|
|
401
|
+
conversionFromStart,
|
|
402
|
+
avgTimeOnStepMs,
|
|
403
|
+
}
|
|
404
|
+
})
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private getDateRange(
|
|
408
|
+
pageViews: PageView[],
|
|
409
|
+
events: CustomEvent[],
|
|
410
|
+
): { start: Date, end: Date } {
|
|
411
|
+
const allTimestamps = [
|
|
412
|
+
...pageViews.map(pv => pv.timestamp),
|
|
413
|
+
...events.map(e => e.timestamp),
|
|
414
|
+
].sort((a, b) => a.getTime() - b.getTime())
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
start: allTimestamps[0] ?? new Date(),
|
|
418
|
+
end: allTimestamps[allTimestamps.length - 1] ?? new Date(),
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// Preset Funnels
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Common e-commerce checkout funnel
|
|
429
|
+
*/
|
|
430
|
+
export const ecommerceCheckoutFunnel: Funnel = createFunnel('ecommerce-checkout')
|
|
431
|
+
.name('E-commerce Checkout')
|
|
432
|
+
.step('view_product', 'View Product', { path: '/products/*' })
|
|
433
|
+
.step('add_to_cart', 'Add to Cart', { eventName: 'add_to_cart' })
|
|
434
|
+
.step('view_cart', 'View Cart', { path: '/cart' })
|
|
435
|
+
.step('begin_checkout', 'Begin Checkout', { path: '/checkout' })
|
|
436
|
+
.step('complete_purchase', 'Complete Purchase', { eventName: 'purchase' })
|
|
437
|
+
.window(60 * 60 * 1000) // 1 hour
|
|
438
|
+
.build()
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* SaaS signup funnel
|
|
442
|
+
*/
|
|
443
|
+
export const saasSignupFunnel: Funnel = createFunnel('saas-signup')
|
|
444
|
+
.name('SaaS Signup')
|
|
445
|
+
.step('landing', 'Landing Page', { path: '/' })
|
|
446
|
+
.step('pricing', 'View Pricing', { path: '/pricing' })
|
|
447
|
+
.step('signup_start', 'Start Signup', { path: '/signup' })
|
|
448
|
+
.step('signup_complete', 'Complete Signup', { eventName: 'signup_complete' })
|
|
449
|
+
.window(24 * 60 * 60 * 1000) // 24 hours
|
|
450
|
+
.build()
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Content engagement funnel
|
|
454
|
+
*/
|
|
455
|
+
export const contentEngagementFunnel: Funnel = createFunnel('content-engagement')
|
|
456
|
+
.name('Content Engagement')
|
|
457
|
+
.step('article_view', 'View Article', { path: '/blog/*' })
|
|
458
|
+
.step('scroll_50', 'Scroll 50%', { eventName: 'scroll_depth', custom: e => 'properties' in e && (e as CustomEvent).properties?.depth === 50 })
|
|
459
|
+
.step('scroll_100', 'Scroll 100%', { eventName: 'scroll_depth', custom: e => 'properties' in e && (e as CustomEvent).properties?.depth === 100 })
|
|
460
|
+
.step('share', 'Share Article', { eventName: 'share' })
|
|
461
|
+
.build()
|
|
462
|
+
|
|
463
|
+
// ============================================================================
|
|
464
|
+
// Utilities
|
|
465
|
+
// ============================================================================
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Calculate drop-off rate between two steps
|
|
469
|
+
*/
|
|
470
|
+
export function calculateDropOffRate(
|
|
471
|
+
stepACount: number,
|
|
472
|
+
stepBCount: number,
|
|
473
|
+
): number {
|
|
474
|
+
if (stepACount === 0) return 0
|
|
475
|
+
return 1 - (stepBCount / stepACount)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Format funnel analysis as a simple report
|
|
480
|
+
*/
|
|
481
|
+
export function formatFunnelReport(analysis: FunnelAnalysis): string {
|
|
482
|
+
const lines: string[] = [
|
|
483
|
+
`Funnel: ${analysis.funnel.name}`,
|
|
484
|
+
`Period: ${analysis.dateRange.start.toISOString()} - ${analysis.dateRange.end.toISOString()}`,
|
|
485
|
+
'',
|
|
486
|
+
`Total Entries: ${analysis.totalEntries}`,
|
|
487
|
+
`Completions: ${analysis.completions}`,
|
|
488
|
+
`Overall Conversion: ${(analysis.conversionRate * 100).toFixed(2)}%`,
|
|
489
|
+
'',
|
|
490
|
+
'Step Breakdown:',
|
|
491
|
+
]
|
|
492
|
+
|
|
493
|
+
for (const step of analysis.steps) {
|
|
494
|
+
lines.push(` ${step.step.name}:`)
|
|
495
|
+
lines.push(` Reached: ${step.reached}`)
|
|
496
|
+
lines.push(` Drop-off: ${step.droppedOff}`)
|
|
497
|
+
lines.push(` Conv. from prev: ${(step.conversionFromPrevious * 100).toFixed(2)}%`)
|
|
498
|
+
lines.push(` Conv. from start: ${(step.conversionFromStart * 100).toFixed(2)}%`)
|
|
499
|
+
if (step.avgTimeOnStepMs !== undefined) {
|
|
500
|
+
lines.push(` Avg time: ${(step.avgTimeOnStepMs / 1000).toFixed(1)}s`)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (analysis.avgCompletionTimeMs !== undefined) {
|
|
505
|
+
lines.push('')
|
|
506
|
+
lines.push(`Avg Completion Time: ${(analysis.avgCompletionTimeMs / 1000).toFixed(1)}s`)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return lines.join('\n')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Identify common drop-off points
|
|
514
|
+
*/
|
|
515
|
+
// eslint-disable-next-line pickier/no-unused-vars
|
|
516
|
+
export function identifyDropOffPoints(analysis: FunnelAnalysis): Array<{
|
|
517
|
+
step: FunnelStep
|
|
518
|
+
dropOffRate: number
|
|
519
|
+
impact: number // How much this affects overall conversion
|
|
520
|
+
}> {
|
|
521
|
+
return analysis.steps
|
|
522
|
+
.map((step, index) => {
|
|
523
|
+
const dropOffRate = 1 - step.conversionFromPrevious
|
|
524
|
+
const previousConversion = index === 0 ? 1 : analysis.steps[index - 1].conversionFromStart
|
|
525
|
+
const impact = dropOffRate * previousConversion
|
|
526
|
+
|
|
527
|
+
return {
|
|
528
|
+
step: step.step,
|
|
529
|
+
dropOffRate,
|
|
530
|
+
impact,
|
|
531
|
+
}
|
|
532
|
+
})
|
|
533
|
+
.sort((a, b) => b.impact - a.impact)
|
|
534
|
+
}
|