@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.
Files changed (237) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +361 -0
  4. package/bin/cli.ts +169 -0
  5. package/dist/Analytics.d.ts +558 -0
  6. package/dist/api.d.ts +109 -0
  7. package/dist/batching.d.ts +93 -0
  8. package/dist/chunk-2mx7fq49.js +4 -0
  9. package/dist/chunk-3z29508k.js +204 -0
  10. package/dist/chunk-deephkz6.js +56 -0
  11. package/dist/chunk-j261vgyp.js +305 -0
  12. package/dist/chunk-xga17tz7.js +207 -0
  13. package/dist/config.d.ts +132 -0
  14. package/dist/dashboard/components/index.d.ts +80 -0
  15. package/dist/dashboard/composables/useAnalytics.d.ts +99 -0
  16. package/dist/dashboard/index.d.ts +110 -0
  17. package/dist/dashboard/types/index.d.ts +144 -0
  18. package/dist/dashboard/utils/index.d.ts +81 -0
  19. package/dist/dynamodb.d.ts +86 -0
  20. package/dist/funnels.d.ts +104 -0
  21. package/dist/geolocation.d.ts +96 -0
  22. package/dist/index.d.ts +371 -0
  23. package/dist/index.js +10211 -0
  24. package/dist/infrastructure/cdk.d.ts +60 -0
  25. package/dist/infrastructure/cloudformation.d.ts +44 -0
  26. package/dist/infrastructure/index.d.ts +45 -0
  27. package/dist/infrastructure/setup.d.ts +133 -0
  28. package/dist/integrations/cloudflare.d.ts +76 -0
  29. package/dist/integrations/hono.d.ts +60 -0
  30. package/dist/integrations/index.d.ts +24 -0
  31. package/dist/integrations/nuxt.d.ts +7 -0
  32. package/dist/integrations/nuxt.js +42 -0
  33. package/dist/integrations/runtime/use-ts-analytics.d.ts +18 -0
  34. package/dist/integrations/runtime/use-ts-analytics.js +17 -0
  35. package/dist/integrations/stx.d.ts +88 -0
  36. package/dist/integrations/stx.js +18 -0
  37. package/dist/lib/crypto-random.d.ts +4 -0
  38. package/dist/lib/salt.d.ts +16 -0
  39. package/dist/local.d.ts +56 -0
  40. package/dist/model-connector.d.ts +145 -0
  41. package/dist/models/AggregatedStats.d.ts +9 -0
  42. package/dist/models/CampaignStats.d.ts +9 -0
  43. package/dist/models/Conversion.d.ts +11 -0
  44. package/dist/models/CustomEvent.d.ts +11 -0
  45. package/dist/models/DeviceStats.d.ts +9 -0
  46. package/dist/models/EventStats.d.ts +9 -0
  47. package/dist/models/GeoStats.d.ts +9 -0
  48. package/dist/models/Goal.d.ts +9 -0
  49. package/dist/models/GoalStats.d.ts +9 -0
  50. package/dist/models/PageStats.d.ts +11 -0
  51. package/dist/models/PageView.d.ts +13 -0
  52. package/dist/models/RealtimeStats.d.ts +9 -0
  53. package/dist/models/ReferrerStats.d.ts +9 -0
  54. package/dist/models/Session.d.ts +11 -0
  55. package/dist/models/Site.d.ts +11 -0
  56. package/dist/models/index.d.ts +28 -0
  57. package/dist/models/types.d.ts +60 -0
  58. package/dist/sqs-buffering.d.ts +243 -0
  59. package/dist/stacks-integration.d.ts +159 -0
  60. package/dist/tracking-script.d.ts +71 -0
  61. package/dist/tracking.d.ts +17 -0
  62. package/dist/tracking.js +26 -0
  63. package/dist/types.d.ts +595 -0
  64. package/dist/utils/geolocation.d.ts +111 -0
  65. package/dist/utils/user-agent.d.ts +17 -0
  66. package/dist/version.d.ts +7 -0
  67. package/package.json +119 -0
  68. package/src/Analytics.ts +3349 -0
  69. package/src/api.ts +1286 -0
  70. package/src/assets/crosswind.css +2220 -0
  71. package/src/batching.ts +452 -0
  72. package/src/components/dashboard/index.ts +18 -0
  73. package/src/config.ts +456 -0
  74. package/src/dashboard/Dashboard.stx +1517 -0
  75. package/src/dashboard/components/AlertCard.stx +177 -0
  76. package/src/dashboard/components/AnalyticsDashboard.stx +354 -0
  77. package/src/dashboard/components/AnimatedNumber.stx +86 -0
  78. package/src/dashboard/components/BarChart.stx +220 -0
  79. package/src/dashboard/components/BrowserBreakdown.stx +98 -0
  80. package/src/dashboard/components/BrowsersTable.stx +125 -0
  81. package/src/dashboard/components/CampaignBreakdown.stx +163 -0
  82. package/src/dashboard/components/CampaignTable.stx +238 -0
  83. package/src/dashboard/components/CountryList.stx +101 -0
  84. package/src/dashboard/components/DataTable.stx +226 -0
  85. package/src/dashboard/components/DateRangePicker.stx +77 -0
  86. package/src/dashboard/components/DeviceBreakdown.stx +94 -0
  87. package/src/dashboard/components/DevicesTable.stx +163 -0
  88. package/src/dashboard/components/DonutChart.stories.ts +55 -0
  89. package/src/dashboard/components/DonutChart.stx +157 -0
  90. package/src/dashboard/components/EmptyState.stx +90 -0
  91. package/src/dashboard/components/EngagementMetrics.stx +183 -0
  92. package/src/dashboard/components/EventsSection.stx +192 -0
  93. package/src/dashboard/components/FilterBar.stx +104 -0
  94. package/src/dashboard/components/FullAnalyticsDashboard.stx +455 -0
  95. package/src/dashboard/components/FunnelChart.stx +142 -0
  96. package/src/dashboard/components/GeoTable.stx +337 -0
  97. package/src/dashboard/components/GoalsPanel.stx +109 -0
  98. package/src/dashboard/components/Header.stx +39 -0
  99. package/src/dashboard/components/HeatmapChart.stx +140 -0
  100. package/src/dashboard/components/LiveActivityFeed.stx +172 -0
  101. package/src/dashboard/components/MetricComparison.stx +106 -0
  102. package/src/dashboard/components/MiniStats.stx +96 -0
  103. package/src/dashboard/components/OSBreakdown.stx +124 -0
  104. package/src/dashboard/components/PageDetailCard.stx +102 -0
  105. package/src/dashboard/components/PagesTable.stx +127 -0
  106. package/src/dashboard/components/ProgressRing.stx +89 -0
  107. package/src/dashboard/components/RealtimeCounter.stories.ts +45 -0
  108. package/src/dashboard/components/RealtimeCounter.stx +46 -0
  109. package/src/dashboard/components/ReferrersTable.stx +180 -0
  110. package/src/dashboard/components/SparklineChart.stx +160 -0
  111. package/src/dashboard/components/StatCard.stories.ts +58 -0
  112. package/src/dashboard/components/StatCard.stx +90 -0
  113. package/src/dashboard/components/SummaryStats.stx +81 -0
  114. package/src/dashboard/components/TabNav.stx +66 -0
  115. package/src/dashboard/components/ThemeSwitcher.stx +124 -0
  116. package/src/dashboard/components/TimeSeriesChart.stx +106 -0
  117. package/src/dashboard/components/TopList.stories.ts +58 -0
  118. package/src/dashboard/components/TopList.stx +74 -0
  119. package/src/dashboard/components/TrendIndicator.stx +84 -0
  120. package/src/dashboard/components/heatmap/ClickHeatmap.stx +264 -0
  121. package/src/dashboard/components/heatmap/ElementClickList.stx +125 -0
  122. package/src/dashboard/components/heatmap/HeatmapControls.stx +125 -0
  123. package/src/dashboard/components/heatmap/HeatmapLegend.stx +69 -0
  124. package/src/dashboard/components/heatmap/PageHeatmap.stx +264 -0
  125. package/src/dashboard/components/heatmap/ScrollHeatmap.stx +127 -0
  126. package/src/dashboard/components/heatmap/index.ts +12 -0
  127. package/src/dashboard/components/index.ts +86 -0
  128. package/src/dashboard/composables/useAnalytics.ts +465 -0
  129. package/src/dashboard/demo/DemoApp.stx +370 -0
  130. package/src/dashboard/demo/index.ts +8 -0
  131. package/src/dashboard/demo/mockData.ts +234 -0
  132. package/src/dashboard/index.ts +117 -0
  133. package/src/dashboard/stx-shim.d.ts +4 -0
  134. package/src/dashboard/types/index.ts +203 -0
  135. package/src/dashboard/utils/index.ts +426 -0
  136. package/src/dynamodb.ts +344 -0
  137. package/src/funnels.ts +534 -0
  138. package/src/geolocation.ts +515 -0
  139. package/src/handlers/alerts.ts +328 -0
  140. package/src/handlers/annotations.ts +128 -0
  141. package/src/handlers/api-keys.ts +240 -0
  142. package/src/handlers/auth.ts +1020 -0
  143. package/src/handlers/authz.ts +137 -0
  144. package/src/handlers/collect.ts +724 -0
  145. package/src/handlers/data.ts +625 -0
  146. package/src/handlers/experiments.ts +138 -0
  147. package/src/handlers/funnels.ts +216 -0
  148. package/src/handlers/goals.ts +218 -0
  149. package/src/handlers/heatmaps.ts +272 -0
  150. package/src/handlers/index.ts +56 -0
  151. package/src/handlers/lib/read-cache.ts +63 -0
  152. package/src/handlers/misc.ts +486 -0
  153. package/src/handlers/oauth.ts +233 -0
  154. package/src/handlers/performance.ts +388 -0
  155. package/src/handlers/sessions.ts +413 -0
  156. package/src/handlers/sharing.ts +198 -0
  157. package/src/handlers/stats.ts +1368 -0
  158. package/src/handlers/team.ts +161 -0
  159. package/src/handlers/uptime.ts +283 -0
  160. package/src/handlers/views.ts +390 -0
  161. package/src/handlers/webhooks.ts +226 -0
  162. package/src/heatmap/index.ts +32 -0
  163. package/src/heatmap/tracking-script.ts +452 -0
  164. package/src/heatmap/types.ts +79 -0
  165. package/src/index.ts +387 -0
  166. package/src/infrastructure/cdk.ts +496 -0
  167. package/src/infrastructure/cloudformation.ts +595 -0
  168. package/src/infrastructure/index.ts +48 -0
  169. package/src/infrastructure/setup.ts +611 -0
  170. package/src/integrations/cloudflare.ts +732 -0
  171. package/src/integrations/hono.ts +589 -0
  172. package/src/integrations/index.ts +27 -0
  173. package/src/integrations/nuxt.ts +78 -0
  174. package/src/integrations/runtime/use-ts-analytics.ts +32 -0
  175. package/src/integrations/stx.ts +138 -0
  176. package/src/jobs/index.ts +127 -0
  177. package/src/lib/crypto-random.ts +41 -0
  178. package/src/lib/ddb-errors.ts +20 -0
  179. package/src/lib/dynamodb.ts +216 -0
  180. package/src/lib/email.ts +38 -0
  181. package/src/lib/ga-import.ts +471 -0
  182. package/src/lib/ga4-api.ts +244 -0
  183. package/src/lib/goals.ts +205 -0
  184. package/src/lib/index.ts +6 -0
  185. package/src/lib/ingest-counters.ts +123 -0
  186. package/src/lib/log.ts +36 -0
  187. package/src/lib/membership.ts +98 -0
  188. package/src/lib/plans.ts +90 -0
  189. package/src/lib/quota.ts +46 -0
  190. package/src/lib/rate-limit.ts +27 -0
  191. package/src/lib/rollups.ts +472 -0
  192. package/src/lib/salt.ts +97 -0
  193. package/src/lib/scheduler.ts +97 -0
  194. package/src/lib/significance.ts +66 -0
  195. package/src/lib/site-retention.ts +56 -0
  196. package/src/local.ts +360 -0
  197. package/src/model-connector.ts +616 -0
  198. package/src/models/AggregatedStats.ts +162 -0
  199. package/src/models/CampaignStats.ts +141 -0
  200. package/src/models/Conversion.ts +135 -0
  201. package/src/models/CustomEvent.ts +123 -0
  202. package/src/models/DeviceStats.ts +105 -0
  203. package/src/models/EventStats.ts +108 -0
  204. package/src/models/GeoStats.ts +112 -0
  205. package/src/models/Goal.ts +105 -0
  206. package/src/models/GoalStats.ts +106 -0
  207. package/src/models/PageStats.ts +152 -0
  208. package/src/models/PageView.ts +277 -0
  209. package/src/models/RealtimeStats.ts +96 -0
  210. package/src/models/ReferrerStats.ts +115 -0
  211. package/src/models/Session.ts +235 -0
  212. package/src/models/Site.ts +115 -0
  213. package/src/models/index.ts +50 -0
  214. package/src/models/orm/index.ts +2043 -0
  215. package/src/models/types.ts +71 -0
  216. package/src/router.ts +521 -0
  217. package/src/sqs-buffering.ts +806 -0
  218. package/src/stacks-integration.ts +527 -0
  219. package/src/tracking-script.ts +944 -0
  220. package/src/tracking.ts +27 -0
  221. package/src/types/analytics.ts +219 -0
  222. package/src/types/api.ts +75 -0
  223. package/src/types/bun-router.d.ts +15 -0
  224. package/src/types/dashboard.ts +62 -0
  225. package/src/types/index.ts +66 -0
  226. package/src/types/stx.d.ts +27 -0
  227. package/src/types/window.d.ts +101 -0
  228. package/src/types.ts +911 -0
  229. package/src/utils/cache.ts +148 -0
  230. package/src/utils/date.ts +118 -0
  231. package/src/utils/filters.ts +71 -0
  232. package/src/utils/geolocation.ts +323 -0
  233. package/src/utils/index.ts +9 -0
  234. package/src/utils/response.ts +180 -0
  235. package/src/utils/timezone-country.ts +242 -0
  236. package/src/utils/user-agent.ts +110 -0
  237. 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
+ }