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