@quantabit/funnel-sdk 1.0.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.
@@ -0,0 +1,1293 @@
1
+ import { BaseApiClient } from '@quantabit/sdk-config';
2
+ import React, { useState, useCallback, useEffect, useMemo } from 'react';
3
+
4
+ /**
5
+ * Funnel SDK - API 客户端 (隐私合规增强版)
6
+ * 漏斗分析系统后端接口封装
7
+ *
8
+ * 隐私保护:
9
+ * - 漏斗步骤追踪需要 analytics 同意
10
+ * - 用户掉漏数据自动脱敏
11
+ * - 分析报告导出支持匿名化
12
+ *
13
+ * 使用 BaseApiClient 基类简化代码
14
+ */
15
+
16
+
17
+ // 可选依赖:隐私合规工具
18
+ let consentManager = null;
19
+ import('@quantabit/sdk-config').then(sdkConfig => {
20
+ consentManager = sdkConfig.consentManager;
21
+ }).catch(() => {
22
+ /* 静默降级 */
23
+ });
24
+
25
+ /**
26
+ * 漏斗 API 客户端
27
+ */
28
+ class FunnelApiClient extends BaseApiClient {
29
+ constructor(config = {}) {
30
+ super("/funnel", config);
31
+ }
32
+
33
+ // ============ 漏斗管理 ============
34
+
35
+ /**
36
+ * 获取漏斗列表
37
+ * @param {Object} params - 查询参数
38
+ */
39
+ async getFunnels(params = {}) {
40
+ return this.get("/list", params);
41
+ }
42
+
43
+ /**
44
+ * 获取漏斗详情
45
+ * @param {string} funnelId - 漏斗 ID
46
+ */
47
+ async getFunnel(funnelId) {
48
+ return this.get(`/${funnelId}`);
49
+ }
50
+
51
+ /**
52
+ * 创建漏斗
53
+ * @param {Object} data - 漏斗数据
54
+ */
55
+ async createFunnel(data) {
56
+ return this.post("/", data);
57
+ }
58
+
59
+ /**
60
+ * 更新漏斗
61
+ * @param {string} funnelId - 漏斗 ID
62
+ * @param {Object} updates - 更新数据
63
+ */
64
+ async updateFunnel(funnelId, updates) {
65
+ return this.put(`/${funnelId}`, updates);
66
+ }
67
+
68
+ /**
69
+ * 删除漏斗
70
+ * @param {string} funnelId - 漏斗 ID
71
+ */
72
+ async deleteFunnel(funnelId) {
73
+ return this.delete(`/${funnelId}`);
74
+ }
75
+
76
+ /**
77
+ * 复制漏斗
78
+ * @param {string} funnelId - 漏斗 ID
79
+ * @param {string} name - 新名称
80
+ */
81
+ async duplicateFunnel(funnelId, name) {
82
+ return this.post(`/${funnelId}/duplicate`, {
83
+ name
84
+ });
85
+ }
86
+
87
+ // ============ 漏斗分析 ============
88
+
89
+ /**
90
+ * 获取漏斗分析
91
+ * @param {string} funnelId - 漏斗 ID
92
+ * @param {Object} params - 分析参数
93
+ */
94
+ async analyze(funnelId, params = {}) {
95
+ // 分析功能需要 analytics 同意
96
+ if (consentManager && !consentManager.hasConsent("analytics")) {
97
+ console.debug("[Funnel] 漏斗分析被跳过(需要 analytics 同意)");
98
+ return {
99
+ code: -1,
100
+ message: "consent_required"
101
+ };
102
+ }
103
+ return this.post(`/${funnelId}/analyze`, params);
104
+ }
105
+
106
+ /**
107
+ * 获取步骤转化率
108
+ * @param {string} funnelId - 漏斗 ID
109
+ * @param {Object} params - 查询参数
110
+ */
111
+ async getConversionRate(funnelId, params = {}) {
112
+ return this.get(`/${funnelId}/conversion`, params);
113
+ }
114
+
115
+ /**
116
+ * 获取趋势数据
117
+ * @param {string} funnelId - 漏斗 ID
118
+ * @param {Object} params - 查询参数
119
+ */
120
+ async getTrend(funnelId, params = {}) {
121
+ return this.get(`/${funnelId}/trend`, params);
122
+ }
123
+
124
+ /**
125
+ * 获取对比分析
126
+ * @param {string} funnelId - 漏斗 ID
127
+ * @param {Object} params - 对比参数
128
+ */
129
+ async compare(funnelId, params = {}) {
130
+ return this.post(`/${funnelId}/compare`, params);
131
+ }
132
+
133
+ // ============ 掉漏分析 ============
134
+
135
+ /**
136
+ * 获取掉漏用户
137
+ * @param {string} funnelId - 漏斗 ID
138
+ * @param {number} stepIndex - 步骤索引
139
+ * @param {Object} params - 查询参数
140
+ */
141
+ async getDropoffUsers(funnelId, stepIndex, params = {}) {
142
+ return this.get(`/${funnelId}/dropoff/${stepIndex}/users`, params);
143
+ }
144
+
145
+ /**
146
+ * 获取掉漏原因分析
147
+ * @param {string} funnelId - 漏斗 ID
148
+ * @param {number} stepIndex - 步骤索引
149
+ */
150
+ async getDropoffReasons(funnelId, stepIndex) {
151
+ return this.get(`/${funnelId}/dropoff/${stepIndex}/reasons`);
152
+ }
153
+
154
+ /**
155
+ * 获取掉漏用户画像
156
+ * @param {string} funnelId - 漏斗 ID
157
+ * @param {number} stepIndex - 步骤索引
158
+ */
159
+ async getDropoffProfile(funnelId, stepIndex) {
160
+ return this.get(`/${funnelId}/dropoff/${stepIndex}/profile`);
161
+ }
162
+
163
+ // ============ 路径分析 ============
164
+
165
+ /**
166
+ * 获取转化路径
167
+ * @param {string} funnelId - 漏斗 ID
168
+ * @param {Object} params - 查询参数
169
+ */
170
+ async getConversionPaths(funnelId, params = {}) {
171
+ return this.get(`/${funnelId}/paths`, params);
172
+ }
173
+
174
+ /**
175
+ * 获取异常路径
176
+ * @param {string} funnelId - 漏斗 ID
177
+ */
178
+ async getAbnormalPaths(funnelId) {
179
+ return this.get(`/${funnelId}/paths/abnormal`);
180
+ }
181
+
182
+ // ============ 分组分析 ============
183
+
184
+ /**
185
+ * 按属性分组分析
186
+ * @param {string} funnelId - 漏斗 ID
187
+ * @param {string} property - 分组属性
188
+ * @param {Object} params - 分析参数
189
+ */
190
+ async analyzeByProperty(funnelId, property, params = {}) {
191
+ return this.post(`/${funnelId}/analyze/group`, {
192
+ property,
193
+ ...params
194
+ });
195
+ }
196
+
197
+ /**
198
+ * 按人群分组分析
199
+ * @param {string} funnelId - 漏斗 ID
200
+ * @param {string[]} segmentIds - 分群 ID 列表
201
+ * @param {Object} params - 分析参数
202
+ */
203
+ async analyzeBySegments(funnelId, segmentIds, params = {}) {
204
+ return this.post(`/${funnelId}/analyze/segments`, {
205
+ segment_ids: segmentIds,
206
+ ...params
207
+ });
208
+ }
209
+
210
+ // ============ 导出 ============
211
+
212
+ /**
213
+ * 导出分析报告
214
+ * @param {string} funnelId - 漏斗 ID
215
+ * @param {Object} options - 导出选项
216
+ */
217
+ async exportReport(funnelId, options = {}) {
218
+ return this.post(`/${funnelId}/export`, options);
219
+ }
220
+
221
+ // ============ 隐私合规扩展 ============
222
+
223
+ /**
224
+ * 漏斗步骤追踪 — 需要 analytics 同意
225
+ * 识别用户进入了漏斗的哪个步骤
226
+ * @param {string} funnelId - 漏斗 ID
227
+ * @param {number} stepIndex - 步骤索引
228
+ * @param {object} metadata - 额外元数据
229
+ */
230
+ async trackStep(funnelId, stepIndex, metadata = {}) {
231
+ if (consentManager && !consentManager.hasConsent("analytics")) {
232
+ return {
233
+ code: -1,
234
+ message: "consent_required"
235
+ };
236
+ }
237
+ return this.post(`/${funnelId}/steps/${stepIndex}/track`, metadata);
238
+ }
239
+
240
+ /**
241
+ * 获取掉漏用户(匿名化) — 降低 PII 暴露风险
242
+ * @param {string} funnelId
243
+ * @param {number} stepIndex
244
+ * @param {object} params
245
+ */
246
+ async getDropoffUsersAnonymized(funnelId, stepIndex, params = {}) {
247
+ const result = await this.getDropoffUsers(funnelId, stepIndex, params);
248
+ if (result?.data?.users) {
249
+ result.data.users = result.data.users.map(u => ({
250
+ ...u,
251
+ user_id: u.user_id ? `user_${u.user_id.toString().slice(-4)}` : "anon",
252
+ email: u.email ? "***@***" : undefined,
253
+ did: u.did ? `${u.did.slice(0, 8)}...` : undefined
254
+ }));
255
+ }
256
+ return result;
257
+ }
258
+
259
+ /**
260
+ * 获取隐私数据声明
261
+ */
262
+ getDataDisclosure() {
263
+ return {
264
+ sdk: "@quantabit/funnel-sdk",
265
+ privacyLevel: "analytics",
266
+ consentRequired: true,
267
+ collected: [{
268
+ type: "funnel_steps",
269
+ description: "Which funnel steps users reach",
270
+ retention: "90 days"
271
+ }, {
272
+ type: "conversion_rate",
273
+ description: "Aggregated conversion metrics",
274
+ retention: "1 year"
275
+ }, {
276
+ type: "dropoff_reasons",
277
+ description: "Why users abandon funnels",
278
+ retention: "90 days"
279
+ }],
280
+ gdprCapabilities: ["delete", "anonymize"],
281
+ anonymization: "User identifiers are hashed in reports; getDropoffUsersAnonymized() strips PII"
282
+ };
283
+ }
284
+
285
+ /**
286
+ * 清除用户漏斗追踪数据 — GDPR Art.17
287
+ */
288
+ async clearData() {
289
+ try {
290
+ return await this.post("/privacy/clear-my-data");
291
+ } catch (e) {
292
+ return {
293
+ success: true,
294
+ note: "Funnel data is aggregated; individual records anonymized"
295
+ };
296
+ }
297
+ }
298
+ }
299
+
300
+ // 创建默认实例
301
+ const funnelApi = new FunnelApiClient();
302
+
303
+ /**
304
+ * Funnel SDK - 类型定义
305
+ */
306
+
307
+ // 漏斗类型
308
+ const FunnelType = {
309
+ REGISTRATION: 'registration',
310
+ PURCHASE: 'purchase',
311
+ ONBOARDING: 'onboarding',
312
+ ACTIVATION: 'activation',
313
+ CUSTOM: 'custom'
314
+ };
315
+
316
+ // 步骤状态
317
+ const StepStatus = {
318
+ NOT_STARTED: 'not_started',
319
+ IN_PROGRESS: 'in_progress',
320
+ COMPLETED: 'completed',
321
+ DROPPED: 'dropped'
322
+ };
323
+
324
+ // 时间窗口
325
+ const TimeWindow = {
326
+ SESSION: 'session',
327
+ DAY: 'day',
328
+ WEEK: 'week',
329
+ MONTH: 'month',
330
+ UNLIMITED: 'unlimited'
331
+ };
332
+
333
+ /**
334
+ * Funnel SDK - 国际化
335
+ * 转化漏斗分析多语言支持
336
+ */
337
+
338
+ const SUPPORTED_LANGUAGES = ['en', 'zh', 'ja', 'ko'];
339
+ const messages = {
340
+ zh: {
341
+ // 漏斗基础
342
+ funnel: '转化漏斗',
343
+ funnels: '漏斗列表',
344
+ createFunnel: '创建漏斗',
345
+ editFunnel: '编辑漏斗',
346
+ deleteFunnel: '删除漏斗',
347
+ funnelName: '漏斗名称',
348
+ funnelDesc: '漏斗描述',
349
+ // 步骤
350
+ step: '步骤',
351
+ steps: '步骤',
352
+ addStep: '添加步骤',
353
+ removeStep: '移除步骤',
354
+ stepName: '步骤名称',
355
+ stepEvent: '步骤事件',
356
+ firstStep: '第一步',
357
+ lastStep: '最后一步',
358
+ // 转化指标
359
+ conversion: '转化',
360
+ conversionRate: '转化率',
361
+ dropOff: '流失',
362
+ dropOffRate: '流失率',
363
+ completed: '完成',
364
+ abandoned: '放弃',
365
+ // 时间
366
+ timeWindow: '时间窗口',
367
+ hour: '小时',
368
+ day: '天',
369
+ week: '周',
370
+ month: '月',
371
+ averageTime: '平均耗时',
372
+ medianTime: '中位耗时',
373
+ // 分析
374
+ analyze: '分析',
375
+ analysis: '漏斗分析',
376
+ comparison: '对比分析',
377
+ trends: '趋势分析',
378
+ breakdown: '细分分析',
379
+ // 维度
380
+ dimension: '维度',
381
+ byChannel: '按渠道',
382
+ byDevice: '按设备',
383
+ byRegion: '按地区',
384
+ bySegment: '按分群',
385
+ // 统计
386
+ totalUsers: '总用户数',
387
+ completedUsers: '完成用户',
388
+ lostUsers: '流失用户',
389
+ overall: '总体转化',
390
+ stepByStep: '逐步转化',
391
+ // 建议
392
+ insights: '洞察',
393
+ recommendations: '优化建议',
394
+ bottleneck: '瓶颈点',
395
+ improvement: '提升空间',
396
+ // 操作
397
+ save: '保存',
398
+ cancel: '取消',
399
+ export: '导出',
400
+ refresh: '刷新',
401
+ // 状态
402
+ loading: '加载中...',
403
+ noData: '暂无数据',
404
+ error: '加载失败',
405
+ // 图表
406
+ chart: '图表',
407
+ bar: '柱状图',
408
+ line: '折线图',
409
+ table: '表格'
410
+ },
411
+ en: {
412
+ funnel: 'Funnel',
413
+ funnels: 'Funnels',
414
+ createFunnel: 'Create Funnel',
415
+ editFunnel: 'Edit Funnel',
416
+ deleteFunnel: 'Delete Funnel',
417
+ funnelName: 'Funnel Name',
418
+ funnelDesc: 'Description',
419
+ step: 'Step',
420
+ steps: 'Steps',
421
+ addStep: 'Add Step',
422
+ removeStep: 'Remove Step',
423
+ stepName: 'Step Name',
424
+ stepEvent: 'Event',
425
+ firstStep: 'First Step',
426
+ lastStep: 'Last Step',
427
+ conversion: 'Conversion',
428
+ conversionRate: 'Conversion Rate',
429
+ dropOff: 'Drop-off',
430
+ dropOffRate: 'Drop-off Rate',
431
+ completed: 'Completed',
432
+ abandoned: 'Abandoned',
433
+ timeWindow: 'Time Window',
434
+ hour: 'Hour',
435
+ day: 'Day',
436
+ week: 'Week',
437
+ month: 'Month',
438
+ averageTime: 'Avg. Time',
439
+ medianTime: 'Median Time',
440
+ analyze: 'Analyze',
441
+ analysis: 'Funnel Analysis',
442
+ comparison: 'Comparison',
443
+ trends: 'Trends',
444
+ breakdown: 'Breakdown',
445
+ dimension: 'Dimension',
446
+ byChannel: 'By Channel',
447
+ byDevice: 'By Device',
448
+ byRegion: 'By Region',
449
+ bySegment: 'By Segment',
450
+ totalUsers: 'Total Users',
451
+ completedUsers: 'Completed',
452
+ lostUsers: 'Lost',
453
+ overall: 'Overall',
454
+ stepByStep: 'Step by Step',
455
+ insights: 'Insights',
456
+ recommendations: 'Recommendations',
457
+ bottleneck: 'Bottleneck',
458
+ improvement: 'Improvement',
459
+ save: 'Save',
460
+ cancel: 'Cancel',
461
+ export: 'Export',
462
+ refresh: 'Refresh',
463
+ loading: 'Loading...',
464
+ noData: 'No Data',
465
+ error: 'Error',
466
+ chart: 'Chart',
467
+ bar: 'Bar',
468
+ line: 'Line',
469
+ table: 'Table'
470
+ },
471
+ ja: {
472
+ funnel: 'ファネル',
473
+ funnels: 'ファネル一覧',
474
+ createFunnel: 'ファネル作成',
475
+ editFunnel: 'ファネル編集',
476
+ deleteFunnel: 'ファネル削除',
477
+ funnelName: 'ファネル名',
478
+ funnelDesc: '説明',
479
+ step: 'ステップ',
480
+ steps: 'ステップ',
481
+ addStep: 'ステップ追加',
482
+ removeStep: 'ステップ削除',
483
+ stepName: 'ステップ名',
484
+ stepEvent: 'イベント',
485
+ firstStep: '最初のステップ',
486
+ lastStep: '最後のステップ',
487
+ conversion: 'コンバージョン',
488
+ conversionRate: 'コンバージョン率',
489
+ dropOff: '離脱',
490
+ dropOffRate: '離脱率',
491
+ completed: '完了',
492
+ abandoned: '離脱',
493
+ timeWindow: '時間範囲',
494
+ hour: '時間',
495
+ day: '日',
496
+ week: '週',
497
+ month: '月',
498
+ averageTime: '平均時間',
499
+ medianTime: '中央値',
500
+ analyze: '分析',
501
+ analysis: 'ファネル分析',
502
+ comparison: '比較分析',
503
+ trends: 'トレンド',
504
+ breakdown: '内訳',
505
+ dimension: 'ディメンション',
506
+ byChannel: 'チャネル別',
507
+ byDevice: 'デバイス別',
508
+ byRegion: '地域別',
509
+ bySegment: 'セグメント別',
510
+ totalUsers: '総ユーザー数',
511
+ completedUsers: '完了ユーザー',
512
+ lostUsers: '離脱ユーザー',
513
+ overall: '全体',
514
+ stepByStep: 'ステップ別',
515
+ insights: 'インサイト',
516
+ recommendations: '改善提案',
517
+ bottleneck: 'ボトルネック',
518
+ improvement: '改善余地',
519
+ save: '保存',
520
+ cancel: 'キャンセル',
521
+ export: 'エクスポート',
522
+ refresh: '更新',
523
+ loading: '読み込み中...',
524
+ noData: 'データなし',
525
+ error: 'エラー',
526
+ chart: 'チャート',
527
+ bar: '棒グラフ',
528
+ line: '折れ線グラフ',
529
+ table: 'テーブル'
530
+ },
531
+ ko: {
532
+ funnel: '퍼널',
533
+ funnels: '퍼널 목록',
534
+ createFunnel: '퍼널 생성',
535
+ editFunnel: '퍼널 편집',
536
+ deleteFunnel: '퍼널 삭제',
537
+ funnelName: '퍼널 이름',
538
+ funnelDesc: '설명',
539
+ step: '단계',
540
+ steps: '단계',
541
+ addStep: '단계 추가',
542
+ removeStep: '단계 제거',
543
+ stepName: '단계 이름',
544
+ stepEvent: '이벤트',
545
+ firstStep: '첫 번째 단계',
546
+ lastStep: '마지막 단계',
547
+ conversion: '전환',
548
+ conversionRate: '전환율',
549
+ dropOff: '이탈',
550
+ dropOffRate: '이탈률',
551
+ completed: '완료',
552
+ abandoned: '이탈',
553
+ timeWindow: '시간 범위',
554
+ hour: '시간',
555
+ day: '일',
556
+ week: '주',
557
+ month: '월',
558
+ averageTime: '평균 시간',
559
+ medianTime: '중앙값',
560
+ analyze: '분석',
561
+ analysis: '퍼널 분석',
562
+ comparison: '비교 분석',
563
+ trends: '트렌드',
564
+ breakdown: '세부 분석',
565
+ dimension: '차원',
566
+ byChannel: '채널별',
567
+ byDevice: '기기별',
568
+ byRegion: '지역별',
569
+ bySegment: '세그먼트별',
570
+ totalUsers: '총 사용자',
571
+ completedUsers: '완료 사용자',
572
+ lostUsers: '이탈 사용자',
573
+ overall: '전체',
574
+ stepByStep: '단계별',
575
+ insights: '인사이트',
576
+ recommendations: '권장 사항',
577
+ bottleneck: '병목 지점',
578
+ improvement: '개선 여지',
579
+ save: '저장',
580
+ cancel: '취소',
581
+ export: '내보내기',
582
+ refresh: '새로고침',
583
+ loading: '로딩 중...',
584
+ noData: '데이터 없음',
585
+ error: '오류',
586
+ chart: '차트',
587
+ bar: '막대 차트',
588
+ line: '선 차트',
589
+ table: '테이블'
590
+ }
591
+ };
592
+ let currentLanguage = 'zh';
593
+ function setLanguage(lang) {
594
+ if (SUPPORTED_LANGUAGES.includes(lang)) currentLanguage = lang;
595
+ }
596
+ function getLanguage() {
597
+ return currentLanguage;
598
+ }
599
+ function t(key) {
600
+ return (messages[currentLanguage] || messages.en)[key] || key;
601
+ }
602
+
603
+ /**
604
+ * Funnel SDK - React Hooks
605
+ * 转化漏斗分析相关的状态管理
606
+ */
607
+
608
+
609
+ /**
610
+ * 获取漏斗列表
611
+ */
612
+ function useFunnels(params = {}) {
613
+ const [funnels, setFunnels] = useState([]);
614
+ const [loading, setLoading] = useState(true);
615
+ const [error, setError] = useState(null);
616
+ const fetchFunnels = useCallback(async () => {
617
+ try {
618
+ setLoading(true);
619
+ const response = await funnelApi.getFunnels(params);
620
+ setFunnels(response.data || []);
621
+ setError(null);
622
+ } catch (err) {
623
+ setError(err.message);
624
+ } finally {
625
+ setLoading(false);
626
+ }
627
+ }, [params]);
628
+ useEffect(() => {
629
+ fetchFunnels();
630
+ }, []);
631
+ return {
632
+ funnels,
633
+ loading,
634
+ error,
635
+ refresh: fetchFunnels
636
+ };
637
+ }
638
+
639
+ /**
640
+ * 获取单个漏斗详情
641
+ */
642
+ function useFunnel(funnelId) {
643
+ const [funnel, setFunnel] = useState(null);
644
+ const [loading, setLoading] = useState(true);
645
+ const [error, setError] = useState(null);
646
+ const fetchFunnel = useCallback(async () => {
647
+ if (!funnelId) return;
648
+ try {
649
+ setLoading(true);
650
+ const response = await funnelApi.getFunnel(funnelId);
651
+ setFunnel(response);
652
+ setError(null);
653
+ } catch (err) {
654
+ setError(err.message);
655
+ } finally {
656
+ setLoading(false);
657
+ }
658
+ }, [funnelId]);
659
+ useEffect(() => {
660
+ fetchFunnel();
661
+ }, [fetchFunnel]);
662
+ return {
663
+ funnel,
664
+ loading,
665
+ error,
666
+ refresh: fetchFunnel
667
+ };
668
+ }
669
+
670
+ /**
671
+ * 漏斗分析
672
+ */
673
+ function useFunnelAnalysis(funnelId, options = {}) {
674
+ const [analysis, setAnalysis] = useState(null);
675
+ const [loading, setLoading] = useState(true);
676
+ const [error, setError] = useState(null);
677
+ const {
678
+ timeWindow = '7d',
679
+ dimension = null,
680
+ segment = null
681
+ } = options;
682
+ const analyze = useCallback(async () => {
683
+ if (!funnelId) return;
684
+ try {
685
+ setLoading(true);
686
+ const response = await funnelApi.analyze(funnelId, {
687
+ timeWindow,
688
+ dimension,
689
+ segment
690
+ });
691
+ setAnalysis(response);
692
+ setError(null);
693
+ } catch (err) {
694
+ setError(err.message);
695
+ } finally {
696
+ setLoading(false);
697
+ }
698
+ }, [funnelId, timeWindow, dimension, segment]);
699
+ useEffect(() => {
700
+ analyze();
701
+ }, [analyze]);
702
+
703
+ // 计算派生数据
704
+ const derivedData = useMemo(() => {
705
+ if (!analysis || !analysis.steps) return null;
706
+ const steps = analysis.steps;
707
+ const totalUsers = steps[0]?.users || 0;
708
+ const completedUsers = steps[steps.length - 1]?.users || 0;
709
+ const overallConversion = totalUsers > 0 ? completedUsers / totalUsers : 0;
710
+
711
+ // 找出最大流失步骤
712
+ let maxDropOffStep = null;
713
+ let maxDropOffRate = 0;
714
+ steps.forEach((step, index) => {
715
+ if (index > 0) {
716
+ const prevUsers = steps[index - 1].users;
717
+ const dropOffRate = prevUsers > 0 ? (prevUsers - step.users) / prevUsers : 0;
718
+ if (dropOffRate > maxDropOffRate) {
719
+ maxDropOffRate = dropOffRate;
720
+ maxDropOffStep = {
721
+ ...step,
722
+ dropOffRate,
723
+ fromStep: steps[index - 1].name
724
+ };
725
+ }
726
+ }
727
+ });
728
+ return {
729
+ totalUsers,
730
+ completedUsers,
731
+ overallConversion,
732
+ bottleneck: maxDropOffStep,
733
+ stepConversions: steps.map((step, index) => ({
734
+ ...step,
735
+ conversionFromPrev: index > 0 && steps[index - 1].users > 0 ? step.users / steps[index - 1].users : 1,
736
+ conversionFromFirst: totalUsers > 0 ? step.users / totalUsers : 0
737
+ }))
738
+ };
739
+ }, [analysis]);
740
+ return {
741
+ analysis,
742
+ derivedData,
743
+ loading,
744
+ error,
745
+ refresh: analyze
746
+ };
747
+ }
748
+
749
+ /**
750
+ * 步骤追踪
751
+ */
752
+ function useStepTracking(funnelId) {
753
+ const [currentStep, setCurrentStep] = useState(0);
754
+ const [trackedSteps, setTrackedSteps] = useState([]);
755
+
756
+ // 追踪步骤
757
+ const trackStep = useCallback(async (stepName, metadata = {}) => {
758
+ try {
759
+ await funnelApi.trackStep(funnelId, stepName, {
760
+ ...metadata,
761
+ timestamp: Date.now()
762
+ });
763
+ setTrackedSteps(prev => [...prev, {
764
+ name: stepName,
765
+ timestamp: Date.now()
766
+ }]);
767
+ setCurrentStep(prev => prev + 1);
768
+ } catch (err) {
769
+ console.error('Track step error:', err);
770
+ }
771
+ }, [funnelId]);
772
+
773
+ // 重置追踪
774
+ const resetTracking = useCallback(() => {
775
+ setCurrentStep(0);
776
+ setTrackedSteps([]);
777
+ }, []);
778
+ return {
779
+ currentStep,
780
+ trackedSteps,
781
+ trackStep,
782
+ resetTracking
783
+ };
784
+ }
785
+
786
+ /**
787
+ * 流失分析
788
+ */
789
+ function useDropOffAnalysis(funnelId, stepIndex, options = {}) {
790
+ const [dropOffData, setDropOffData] = useState(null);
791
+ const [loading, setLoading] = useState(true);
792
+ const [error, setError] = useState(null);
793
+ const analyze = useCallback(async () => {
794
+ if (!funnelId || stepIndex === undefined) return;
795
+ try {
796
+ setLoading(true);
797
+ const response = await funnelApi.getDropOff(funnelId, stepIndex, options);
798
+ setDropOffData(response);
799
+ setError(null);
800
+ } catch (err) {
801
+ setError(err.message);
802
+ } finally {
803
+ setLoading(false);
804
+ }
805
+ }, [funnelId, stepIndex, options]);
806
+ useEffect(() => {
807
+ analyze();
808
+ }, [analyze]);
809
+ return {
810
+ dropOffData,
811
+ loading,
812
+ error,
813
+ refresh: analyze
814
+ };
815
+ }
816
+
817
+ /**
818
+ * 漏斗对比分析
819
+ */
820
+ function useFunnelComparison(funnelId, compareOptions = {}) {
821
+ const [comparison, setComparison] = useState(null);
822
+ const [loading, setLoading] = useState(true);
823
+ const [error, setError] = useState(null);
824
+ const {
825
+ segments = [],
826
+ timeRanges = []
827
+ } = compareOptions;
828
+ const compare = useCallback(async () => {
829
+ if (!funnelId) return;
830
+ try {
831
+ setLoading(true);
832
+ const response = await funnelApi.compare(funnelId, {
833
+ segments,
834
+ timeRanges
835
+ });
836
+ setComparison(response);
837
+ setError(null);
838
+ } catch (err) {
839
+ setError(err.message);
840
+ } finally {
841
+ setLoading(false);
842
+ }
843
+ }, [funnelId, segments, timeRanges]);
844
+ useEffect(() => {
845
+ compare();
846
+ }, [compare]);
847
+ return {
848
+ comparison,
849
+ loading,
850
+ error,
851
+ refresh: compare
852
+ };
853
+ }
854
+
855
+ /**
856
+ * 漏斗操作(创建、更新、删除)
857
+ */
858
+ function useFunnelActions() {
859
+ const [loading, setLoading] = useState(false);
860
+ const [error, setError] = useState(null);
861
+ const createFunnel = useCallback(async data => {
862
+ try {
863
+ setLoading(true);
864
+ setError(null);
865
+ const result = await funnelApi.createFunnel(data);
866
+ return result;
867
+ } catch (err) {
868
+ setError(err.message);
869
+ throw err;
870
+ } finally {
871
+ setLoading(false);
872
+ }
873
+ }, []);
874
+ const updateFunnel = useCallback(async (funnelId, data) => {
875
+ try {
876
+ setLoading(true);
877
+ setError(null);
878
+ const result = await funnelApi.updateFunnel(funnelId, data);
879
+ return result;
880
+ } catch (err) {
881
+ setError(err.message);
882
+ throw err;
883
+ } finally {
884
+ setLoading(false);
885
+ }
886
+ }, []);
887
+ const deleteFunnel = useCallback(async funnelId => {
888
+ try {
889
+ setLoading(true);
890
+ setError(null);
891
+ await funnelApi.deleteFunnel(funnelId);
892
+ } catch (err) {
893
+ setError(err.message);
894
+ throw err;
895
+ } finally {
896
+ setLoading(false);
897
+ }
898
+ }, []);
899
+ return {
900
+ loading,
901
+ error,
902
+ createFunnel,
903
+ updateFunnel,
904
+ deleteFunnel
905
+ };
906
+ }
907
+
908
+ /**
909
+ * Funnel SDK - React 组件
910
+ * 转化漏斗可视化组件
911
+ */
912
+
913
+
914
+ /**
915
+ * 漏斗图组件
916
+ */
917
+ function FunnelChart({
918
+ funnelId,
919
+ steps,
920
+ showLabels = true,
921
+ animated = true
922
+ }) {
923
+ const {
924
+ derivedData,
925
+ loading,
926
+ error
927
+ } = useFunnelAnalysis(funnelId);
928
+ const stepsData = steps || derivedData?.stepConversions || [];
929
+ const maxUsers = stepsData[0]?.users || 1;
930
+ if (loading) {
931
+ return /*#__PURE__*/React.createElement("div", {
932
+ className: "eco-funnel-loading"
933
+ }, /*#__PURE__*/React.createElement("div", {
934
+ className: "eco-funnel-spinner"
935
+ }), /*#__PURE__*/React.createElement("span", null, t('loading')));
936
+ }
937
+ if (error) {
938
+ return /*#__PURE__*/React.createElement("div", {
939
+ className: "eco-funnel-error"
940
+ }, error);
941
+ }
942
+ return /*#__PURE__*/React.createElement("div", {
943
+ className: "eco-funnel-chart"
944
+ }, /*#__PURE__*/React.createElement("div", {
945
+ className: "eco-funnel-visual"
946
+ }, stepsData.map((step, index) => {
947
+ const widthPercent = step.users / maxUsers * 100;
948
+ const conversionRate = step.conversionFromFirst * 100;
949
+ return /*#__PURE__*/React.createElement("div", {
950
+ key: step.id || index,
951
+ className: `eco-funnel-step ${animated ? 'animated' : ''}`,
952
+ style: {
953
+ '--step-width': `${widthPercent}%`,
954
+ '--step-delay': `${index * 0.1}s`
955
+ }
956
+ }, /*#__PURE__*/React.createElement("div", {
957
+ className: "eco-funnel-step-bar"
958
+ }, /*#__PURE__*/React.createElement("div", {
959
+ className: "eco-funnel-step-fill"
960
+ })), showLabels && /*#__PURE__*/React.createElement("div", {
961
+ className: "eco-funnel-step-info"
962
+ }, /*#__PURE__*/React.createElement("span", {
963
+ className: "eco-funnel-step-name"
964
+ }, step.name), /*#__PURE__*/React.createElement("span", {
965
+ className: "eco-funnel-step-users"
966
+ }, step.users.toLocaleString()), /*#__PURE__*/React.createElement("span", {
967
+ className: "eco-funnel-step-rate"
968
+ }, conversionRate.toFixed(1), "%")), index > 0 && /*#__PURE__*/React.createElement("div", {
969
+ className: "eco-funnel-dropoff"
970
+ }, /*#__PURE__*/React.createElement("span", {
971
+ className: "eco-funnel-dropoff-icon"
972
+ }, "\u2193"), /*#__PURE__*/React.createElement("span", {
973
+ className: "eco-funnel-dropoff-rate"
974
+ }, "-", ((1 - step.conversionFromPrev) * 100).toFixed(1), "%")));
975
+ })));
976
+ }
977
+
978
+ /**
979
+ * 漏斗步骤卡片
980
+ */
981
+ function FunnelStepCard({
982
+ step,
983
+ index,
984
+ isBottleneck = false,
985
+ onClick
986
+ }) {
987
+ return /*#__PURE__*/React.createElement("div", {
988
+ className: `eco-funnel-step-card ${isBottleneck ? 'bottleneck' : ''}`,
989
+ onClick: () => onClick?.(step, index)
990
+ }, /*#__PURE__*/React.createElement("div", {
991
+ className: "eco-funnel-step-header"
992
+ }, /*#__PURE__*/React.createElement("span", {
993
+ className: "eco-funnel-step-index"
994
+ }, index + 1), /*#__PURE__*/React.createElement("h4", {
995
+ className: "eco-funnel-step-title"
996
+ }, step.name), isBottleneck && /*#__PURE__*/React.createElement("span", {
997
+ className: "eco-funnel-bottleneck-badge"
998
+ }, "\uD83D\uDD34 ", t('bottleneck'))), /*#__PURE__*/React.createElement("div", {
999
+ className: "eco-funnel-step-metrics"
1000
+ }, /*#__PURE__*/React.createElement("div", {
1001
+ className: "eco-funnel-metric"
1002
+ }, /*#__PURE__*/React.createElement("span", {
1003
+ className: "eco-funnel-metric-value"
1004
+ }, step.users.toLocaleString()), /*#__PURE__*/React.createElement("span", {
1005
+ className: "eco-funnel-metric-label"
1006
+ }, t('totalUsers'))), /*#__PURE__*/React.createElement("div", {
1007
+ className: "eco-funnel-metric"
1008
+ }, /*#__PURE__*/React.createElement("span", {
1009
+ className: "eco-funnel-metric-value"
1010
+ }, (step.conversionRate * 100).toFixed(1), "%"), /*#__PURE__*/React.createElement("span", {
1011
+ className: "eco-funnel-metric-label"
1012
+ }, t('conversionRate'))), step.dropOffRate !== undefined && /*#__PURE__*/React.createElement("div", {
1013
+ className: "eco-funnel-metric negative"
1014
+ }, /*#__PURE__*/React.createElement("span", {
1015
+ className: "eco-funnel-metric-value"
1016
+ }, "-", (step.dropOffRate * 100).toFixed(1), "%"), /*#__PURE__*/React.createElement("span", {
1017
+ className: "eco-funnel-metric-label"
1018
+ }, t('dropOffRate')))), step.averageTime && /*#__PURE__*/React.createElement("div", {
1019
+ className: "eco-funnel-step-time"
1020
+ }, /*#__PURE__*/React.createElement("span", null, t('averageTime'), ": ", formatDuration(step.averageTime))));
1021
+ }
1022
+
1023
+ /**
1024
+ * 漏斗概览卡片
1025
+ */
1026
+ function FunnelOverview({
1027
+ funnelId,
1028
+ title
1029
+ }) {
1030
+ const {
1031
+ analysis,
1032
+ derivedData,
1033
+ loading,
1034
+ error
1035
+ } = useFunnelAnalysis(funnelId);
1036
+ if (loading) {
1037
+ return /*#__PURE__*/React.createElement("div", {
1038
+ className: "eco-funnel-overview eco-funnel-loading"
1039
+ }, /*#__PURE__*/React.createElement("div", {
1040
+ className: "eco-funnel-spinner"
1041
+ }));
1042
+ }
1043
+ if (error) {
1044
+ return /*#__PURE__*/React.createElement("div", {
1045
+ className: "eco-funnel-overview eco-funnel-error"
1046
+ }, error);
1047
+ }
1048
+ if (!derivedData) {
1049
+ return /*#__PURE__*/React.createElement("div", {
1050
+ className: "eco-funnel-overview eco-funnel-empty"
1051
+ }, t('noData'));
1052
+ }
1053
+ const {
1054
+ totalUsers,
1055
+ completedUsers,
1056
+ overallConversion,
1057
+ bottleneck
1058
+ } = derivedData;
1059
+ return /*#__PURE__*/React.createElement("div", {
1060
+ className: "eco-funnel-overview"
1061
+ }, /*#__PURE__*/React.createElement("h3", {
1062
+ className: "eco-funnel-overview-title"
1063
+ }, title || t('analysis')), /*#__PURE__*/React.createElement("div", {
1064
+ className: "eco-funnel-overview-stats"
1065
+ }, /*#__PURE__*/React.createElement("div", {
1066
+ className: "eco-funnel-stat"
1067
+ }, /*#__PURE__*/React.createElement("span", {
1068
+ className: "eco-funnel-stat-value"
1069
+ }, totalUsers.toLocaleString()), /*#__PURE__*/React.createElement("span", {
1070
+ className: "eco-funnel-stat-label"
1071
+ }, t('totalUsers'))), /*#__PURE__*/React.createElement("div", {
1072
+ className: "eco-funnel-stat"
1073
+ }, /*#__PURE__*/React.createElement("span", {
1074
+ className: "eco-funnel-stat-value"
1075
+ }, completedUsers.toLocaleString()), /*#__PURE__*/React.createElement("span", {
1076
+ className: "eco-funnel-stat-label"
1077
+ }, t('completedUsers'))), /*#__PURE__*/React.createElement("div", {
1078
+ className: "eco-funnel-stat highlight"
1079
+ }, /*#__PURE__*/React.createElement("span", {
1080
+ className: "eco-funnel-stat-value"
1081
+ }, (overallConversion * 100).toFixed(1), "%"), /*#__PURE__*/React.createElement("span", {
1082
+ className: "eco-funnel-stat-label"
1083
+ }, t('overall'), " ", t('conversionRate')))), bottleneck && /*#__PURE__*/React.createElement("div", {
1084
+ className: "eco-funnel-bottleneck-info"
1085
+ }, /*#__PURE__*/React.createElement("h4", null, "\uD83D\uDD34 ", t('bottleneck')), /*#__PURE__*/React.createElement("p", null, bottleneck.fromStep, " \u2192 ", bottleneck.name, ":", /*#__PURE__*/React.createElement("strong", null, " -", (bottleneck.dropOffRate * 100).toFixed(1), "%"), " ", t('dropOff'))));
1086
+ }
1087
+
1088
+ /**
1089
+ * 漏斗对比图
1090
+ */
1091
+ function FunnelComparison({
1092
+ data,
1093
+ labels
1094
+ }) {
1095
+ if (!data || data.length === 0) {
1096
+ return /*#__PURE__*/React.createElement("div", {
1097
+ className: "eco-funnel-empty"
1098
+ }, t('noData'));
1099
+ }
1100
+ const colors = ['#6366f1', '#10b981', '#f59e0b', '#ef4444'];
1101
+ return /*#__PURE__*/React.createElement("div", {
1102
+ className: "eco-funnel-comparison"
1103
+ }, /*#__PURE__*/React.createElement("div", {
1104
+ className: "eco-funnel-comparison-legend"
1105
+ }, labels.map((label, i) => /*#__PURE__*/React.createElement("div", {
1106
+ key: i,
1107
+ className: "eco-funnel-legend-item"
1108
+ }, /*#__PURE__*/React.createElement("span", {
1109
+ className: "eco-funnel-legend-color",
1110
+ style: {
1111
+ backgroundColor: colors[i % colors.length]
1112
+ }
1113
+ }), /*#__PURE__*/React.createElement("span", null, label)))), /*#__PURE__*/React.createElement("div", {
1114
+ className: "eco-funnel-comparison-chart"
1115
+ }, data[0]?.steps?.map((_, stepIndex) => /*#__PURE__*/React.createElement("div", {
1116
+ key: stepIndex,
1117
+ className: "eco-funnel-comparison-row"
1118
+ }, /*#__PURE__*/React.createElement("div", {
1119
+ className: "eco-funnel-comparison-label"
1120
+ }, data[0].steps[stepIndex].name), /*#__PURE__*/React.createElement("div", {
1121
+ className: "eco-funnel-comparison-bars"
1122
+ }, data.map((funnel, funnelIndex) => {
1123
+ const step = funnel.steps[stepIndex];
1124
+ const rate = step.conversionRate * 100;
1125
+ return /*#__PURE__*/React.createElement("div", {
1126
+ key: funnelIndex,
1127
+ className: "eco-funnel-comparison-bar",
1128
+ style: {
1129
+ width: `${rate}%`,
1130
+ backgroundColor: colors[funnelIndex % colors.length]
1131
+ }
1132
+ }, /*#__PURE__*/React.createElement("span", {
1133
+ className: "eco-funnel-comparison-value"
1134
+ }, rate.toFixed(1), "%"));
1135
+ }))))));
1136
+ }
1137
+
1138
+ /**
1139
+ * 漏斗构建器
1140
+ */
1141
+ function FunnelBuilder({
1142
+ initialSteps = [],
1143
+ onSave,
1144
+ onCancel
1145
+ }) {
1146
+ const [steps, setSteps] = useState(initialSteps.length > 0 ? initialSteps : [{
1147
+ name: '',
1148
+ event: ''
1149
+ }, {
1150
+ name: '',
1151
+ event: ''
1152
+ }]);
1153
+ const [funnelName, setFunnelName] = useState('');
1154
+ const {
1155
+ loading,
1156
+ createFunnel
1157
+ } = useFunnelActions();
1158
+ const addStep = () => {
1159
+ setSteps([...steps, {
1160
+ name: '',
1161
+ event: ''
1162
+ }]);
1163
+ };
1164
+ const removeStep = index => {
1165
+ if (steps.length > 2) {
1166
+ setSteps(steps.filter((_, i) => i !== index));
1167
+ }
1168
+ };
1169
+ const updateStep = (index, field, value) => {
1170
+ const newSteps = [...steps];
1171
+ newSteps[index] = {
1172
+ ...newSteps[index],
1173
+ [field]: value
1174
+ };
1175
+ setSteps(newSteps);
1176
+ };
1177
+ const handleSave = async () => {
1178
+ try {
1179
+ const result = await createFunnel({
1180
+ name: funnelName,
1181
+ steps: steps.filter(s => s.name && s.event)
1182
+ });
1183
+ onSave?.(result);
1184
+ } catch (err) {
1185
+ console.error('Save funnel error:', err);
1186
+ }
1187
+ };
1188
+ return /*#__PURE__*/React.createElement("div", {
1189
+ className: "eco-funnel-builder"
1190
+ }, /*#__PURE__*/React.createElement("div", {
1191
+ className: "eco-funnel-builder-header"
1192
+ }, /*#__PURE__*/React.createElement("input", {
1193
+ type: "text",
1194
+ className: "eco-funnel-name-input",
1195
+ placeholder: t('funnelName'),
1196
+ value: funnelName,
1197
+ onChange: e => setFunnelName(e.target.value)
1198
+ })), /*#__PURE__*/React.createElement("div", {
1199
+ className: "eco-funnel-builder-steps"
1200
+ }, steps.map((step, index) => /*#__PURE__*/React.createElement("div", {
1201
+ key: index,
1202
+ className: "eco-funnel-builder-step"
1203
+ }, /*#__PURE__*/React.createElement("div", {
1204
+ className: "eco-funnel-step-number"
1205
+ }, index + 1), /*#__PURE__*/React.createElement("div", {
1206
+ className: "eco-funnel-step-inputs"
1207
+ }, /*#__PURE__*/React.createElement("input", {
1208
+ type: "text",
1209
+ placeholder: t('stepName'),
1210
+ value: step.name,
1211
+ onChange: e => updateStep(index, 'name', e.target.value)
1212
+ }), /*#__PURE__*/React.createElement("input", {
1213
+ type: "text",
1214
+ placeholder: t('stepEvent'),
1215
+ value: step.event,
1216
+ onChange: e => updateStep(index, 'event', e.target.value)
1217
+ })), steps.length > 2 && /*#__PURE__*/React.createElement("button", {
1218
+ className: "eco-funnel-remove-btn",
1219
+ onClick: () => removeStep(index)
1220
+ }, "\u2715")))), /*#__PURE__*/React.createElement("button", {
1221
+ className: "eco-funnel-add-step-btn",
1222
+ onClick: addStep
1223
+ }, "+ ", t('addStep')), /*#__PURE__*/React.createElement("div", {
1224
+ className: "eco-funnel-builder-actions"
1225
+ }, /*#__PURE__*/React.createElement("button", {
1226
+ className: "eco-funnel-btn",
1227
+ onClick: onCancel
1228
+ }, t('cancel')), /*#__PURE__*/React.createElement("button", {
1229
+ className: "eco-funnel-btn eco-funnel-btn-primary",
1230
+ onClick: handleSave,
1231
+ disabled: loading || !funnelName || steps.some(s => !s.name || !s.event)
1232
+ }, loading ? t('loading') : t('save'))));
1233
+ }
1234
+
1235
+ /**
1236
+ * 流失详情面板
1237
+ */
1238
+ function DropOffPanel({
1239
+ funnelId,
1240
+ stepIndex,
1241
+ stepName
1242
+ }) {
1243
+ const [activeTab, setActiveTab] = useState('reasons');
1244
+ return /*#__PURE__*/React.createElement("div", {
1245
+ className: "eco-funnel-dropoff-panel"
1246
+ }, /*#__PURE__*/React.createElement("div", {
1247
+ className: "eco-funnel-dropoff-header"
1248
+ }, /*#__PURE__*/React.createElement("h4", null, t('dropOff'), " - ", stepName)), /*#__PURE__*/React.createElement("div", {
1249
+ className: "eco-funnel-dropoff-tabs"
1250
+ }, /*#__PURE__*/React.createElement("button", {
1251
+ className: activeTab === 'reasons' ? 'active' : '',
1252
+ onClick: () => setActiveTab('reasons')
1253
+ }, t('insights')), /*#__PURE__*/React.createElement("button", {
1254
+ className: activeTab === 'users' ? 'active' : '',
1255
+ onClick: () => setActiveTab('users')
1256
+ }, t('lostUsers')), /*#__PURE__*/React.createElement("button", {
1257
+ className: activeTab === 'recommendations' ? 'active' : '',
1258
+ onClick: () => setActiveTab('recommendations')
1259
+ }, t('recommendations'))), /*#__PURE__*/React.createElement("div", {
1260
+ className: "eco-funnel-dropoff-content"
1261
+ }, activeTab === 'reasons' && /*#__PURE__*/React.createElement("div", {
1262
+ className: "eco-funnel-insights"
1263
+ }, /*#__PURE__*/React.createElement("p", null, "\u5206\u6790\u6D41\u5931\u7528\u6237\u7684\u884C\u4E3A\u6A21\u5F0F...")), activeTab === 'users' && /*#__PURE__*/React.createElement("div", {
1264
+ className: "eco-funnel-lost-users"
1265
+ }, /*#__PURE__*/React.createElement("p", null, "\u5C55\u793A\u6D41\u5931\u7528\u6237\u5217\u8868...")), activeTab === 'recommendations' && /*#__PURE__*/React.createElement("div", {
1266
+ className: "eco-funnel-recommendations"
1267
+ }, /*#__PURE__*/React.createElement("p", null, "\u4F18\u5316\u5EFA\u8BAE..."))));
1268
+ }
1269
+
1270
+ // 工具函数
1271
+ function formatDuration(ms) {
1272
+ if (ms < 1000) return `${ms}ms`;
1273
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
1274
+ if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
1275
+ return `${(ms / 3600000).toFixed(1)}h`;
1276
+ }
1277
+
1278
+ /**
1279
+ * @quantabit/funnel-sdk
1280
+ * Funnel Analysis SDK - Full Version
1281
+ */
1282
+
1283
+ const analyze = (...args) => funnelApi.analyze(...args);
1284
+ const getDropOff = (...args) => funnelApi.getDropOff(...args);
1285
+ const trackStep = (...args) => funnelApi.trackStep(...args);
1286
+ const getFunnels = (...args) => funnelApi.getFunnels(...args);
1287
+ const getFunnel = (...args) => funnelApi.getFunnel(...args);
1288
+ const createFunnel = (...args) => funnelApi.createFunnel(...args);
1289
+ const updateFunnel = (...args) => funnelApi.updateFunnel(...args);
1290
+ const deleteFunnel = (...args) => funnelApi.deleteFunnel(...args);
1291
+
1292
+ export { DropOffPanel, FunnelApiClient, FunnelBuilder, FunnelChart, FunnelComparison, FunnelOverview, FunnelStepCard, FunnelType, SUPPORTED_LANGUAGES, StepStatus, TimeWindow, analyze, createFunnel, deleteFunnel, funnelApi, getDropOff, getFunnel, getFunnels, getLanguage, messages, setLanguage, t, trackStep, updateFunnel, useDropOffAnalysis, useFunnel, useFunnelActions, useFunnelAnalysis, useFunnelComparison, useFunnels, useStepTracking };
1293
+ //# sourceMappingURL=index.esm.js.map