@feiyangdefeng/sdk 1.0.0 → 1.1.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/README.md +9 -7
- package/dist/collector/auto-tracker.d.ts +4 -0
- package/dist/collector/auto-tracker.d.ts.map +1 -1
- package/dist/collector/click-tracker.d.ts +10 -1
- package/dist/collector/click-tracker.d.ts.map +1 -1
- package/dist/collector/error-tracker.d.ts.map +1 -1
- package/dist/collector/exposure-tracker.d.ts.map +1 -1
- package/dist/collector/input-tracker.d.ts +10 -1
- package/dist/collector/input-tracker.d.ts.map +1 -1
- package/dist/collector/performance-tracker.d.ts.map +1 -1
- package/dist/collector/view-tracker.d.ts.map +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/panel-manager.d.ts +27 -0
- package/dist/core/panel-manager.d.ts.map +1 -0
- package/dist/core/request.d.ts +3 -0
- package/dist/core/request.d.ts.map +1 -1
- package/dist/core/utils.d.ts +5 -0
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/core/vue-utils.d.ts +58 -0
- package/dist/core/vue-utils.d.ts.map +1 -0
- package/dist/extension/naive-ui-tracker.d.ts +20 -0
- package/dist/extension/naive-ui-tracker.d.ts.map +1 -0
- package/dist/extension/performance-monitor.d.ts +93 -0
- package/dist/extension/performance-monitor.d.ts.map +1 -0
- package/dist/extension/user-action-sequence.d.ts +151 -0
- package/dist/extension/user-action-sequence.d.ts.map +1 -0
- package/dist/extension/vue-integration.d.ts +28 -0
- package/dist/extension/vue-integration.d.ts.map +1 -0
- package/dist/extension/vue-plugin.d.ts +66 -0
- package/dist/extension/vue-plugin.d.ts.map +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.es.js +3293 -246
- package/dist/index.es.js.map +1 -1
- package/dist/index.iife.js +2 -2
- package/dist/index.iife.js.map +1 -1
- package/dist/index.umd.js +2 -2
- package/dist/index.umd.js.map +1 -1
- package/dist/panels/offline-storage-viewer.html.d.ts +6 -0
- package/dist/panels/offline-storage-viewer.html.d.ts.map +1 -0
- package/dist/panels/performance-panel.html.d.ts +6 -0
- package/dist/panels/performance-panel.html.d.ts.map +1 -0
- package/dist/processor/deduplicator.d.ts.map +1 -1
- package/dist/tracker.d.ts +24 -0
- package/dist/tracker.d.ts.map +1 -1
- package/dist/transport/batch-sender.d.ts.map +1 -1
- package/dist/transport/retry-handler.d.ts.map +1 -1
- package/package.json +2 -5
package/dist/index.es.js
CHANGED
|
@@ -16,7 +16,12 @@ const defaultConfig = {
|
|
|
16
16
|
maxRetries: 3,
|
|
17
17
|
userId: "",
|
|
18
18
|
deviceId: "",
|
|
19
|
-
customProperties: {}
|
|
19
|
+
customProperties: {},
|
|
20
|
+
enableVueTracking: false,
|
|
21
|
+
enableNaiveUITracking: false,
|
|
22
|
+
componentInfoDepth: 3,
|
|
23
|
+
extractComponentProps: true,
|
|
24
|
+
extractComponentValue: true
|
|
20
25
|
};
|
|
21
26
|
class ConfigManager {
|
|
22
27
|
constructor(config) {
|
|
@@ -133,8 +138,16 @@ function getDeviceId() {
|
|
|
133
138
|
if (typeof window === "undefined") {
|
|
134
139
|
return generateId();
|
|
135
140
|
}
|
|
141
|
+
if (typeof localStorage === "undefined" || !localStorage) {
|
|
142
|
+
return generateId();
|
|
143
|
+
}
|
|
136
144
|
const key = "__tracker_device_id__";
|
|
137
|
-
let deviceId =
|
|
145
|
+
let deviceId = null;
|
|
146
|
+
try {
|
|
147
|
+
deviceId = localStorage.getItem(key);
|
|
148
|
+
} catch (e) {
|
|
149
|
+
return generateId();
|
|
150
|
+
}
|
|
138
151
|
if (!deviceId) {
|
|
139
152
|
deviceId = generateId();
|
|
140
153
|
try {
|
|
@@ -148,8 +161,16 @@ function getSessionId() {
|
|
|
148
161
|
if (typeof window === "undefined") {
|
|
149
162
|
return generateId();
|
|
150
163
|
}
|
|
164
|
+
if (typeof sessionStorage === "undefined" || !sessionStorage) {
|
|
165
|
+
return generateId();
|
|
166
|
+
}
|
|
151
167
|
const key = "__tracker_session_id__";
|
|
152
|
-
let sessionId =
|
|
168
|
+
let sessionId = null;
|
|
169
|
+
try {
|
|
170
|
+
sessionId = sessionStorage.getItem(key);
|
|
171
|
+
} catch (e) {
|
|
172
|
+
return generateId();
|
|
173
|
+
}
|
|
153
174
|
if (!sessionId) {
|
|
154
175
|
sessionId = generateId();
|
|
155
176
|
try {
|
|
@@ -165,6 +186,12 @@ function getCurrentUrl() {
|
|
|
165
186
|
}
|
|
166
187
|
return window.location.href;
|
|
167
188
|
}
|
|
189
|
+
function isTrackerInternalPanel() {
|
|
190
|
+
if (typeof window === "undefined") {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return !!window.__trackerInternalPanel;
|
|
194
|
+
}
|
|
168
195
|
function getPageTitle() {
|
|
169
196
|
if (typeof window === "undefined") {
|
|
170
197
|
return "";
|
|
@@ -213,22 +240,33 @@ function throttle(func, wait) {
|
|
|
213
240
|
};
|
|
214
241
|
}
|
|
215
242
|
function deepClone(obj) {
|
|
243
|
+
return deepCloneWithRefs(obj, /* @__PURE__ */ new WeakSet());
|
|
244
|
+
}
|
|
245
|
+
function deepCloneWithRefs(obj, visited) {
|
|
216
246
|
if (obj === null || typeof obj !== "object") {
|
|
217
247
|
return obj;
|
|
218
248
|
}
|
|
249
|
+
if (visited.has(obj)) {
|
|
250
|
+
return { __circular: true };
|
|
251
|
+
}
|
|
219
252
|
if (obj instanceof Date) {
|
|
220
253
|
return new Date(obj.getTime());
|
|
221
254
|
}
|
|
222
255
|
if (obj instanceof Array) {
|
|
223
|
-
|
|
256
|
+
visited.add(obj);
|
|
257
|
+
const cloned = obj.map((item) => deepCloneWithRefs(item, visited));
|
|
258
|
+
visited.delete(obj);
|
|
259
|
+
return cloned;
|
|
224
260
|
}
|
|
225
261
|
if (typeof obj === "object") {
|
|
262
|
+
visited.add(obj);
|
|
226
263
|
const clonedObj = {};
|
|
227
264
|
for (const key in obj) {
|
|
228
265
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
229
|
-
clonedObj[key] =
|
|
266
|
+
clonedObj[key] = deepCloneWithRefs(obj[key], visited);
|
|
230
267
|
}
|
|
231
268
|
}
|
|
269
|
+
visited.delete(obj);
|
|
232
270
|
return clonedObj;
|
|
233
271
|
}
|
|
234
272
|
return obj;
|
|
@@ -312,6 +350,15 @@ class ViewTracker {
|
|
|
312
350
|
* 追踪页面浏览
|
|
313
351
|
*/
|
|
314
352
|
trackPageView() {
|
|
353
|
+
if (isTrackerInternalPanel()) {
|
|
354
|
+
return {
|
|
355
|
+
type: "pageview",
|
|
356
|
+
name: "pageview",
|
|
357
|
+
timestamp: Date.now(),
|
|
358
|
+
url: getCurrentUrl(),
|
|
359
|
+
title: getPageTitle()
|
|
360
|
+
};
|
|
361
|
+
}
|
|
315
362
|
if (this.isTracking) {
|
|
316
363
|
return {
|
|
317
364
|
type: "pageview",
|
|
@@ -504,6 +551,539 @@ class ViewTracker {
|
|
|
504
551
|
}
|
|
505
552
|
}
|
|
506
553
|
}
|
|
554
|
+
function getVueComponentInstance(element) {
|
|
555
|
+
if (!element || typeof window === "undefined") {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const vueInstance = element;
|
|
559
|
+
if (vueInstance.__vueParentComponent) {
|
|
560
|
+
return vueInstance.__vueParentComponent;
|
|
561
|
+
}
|
|
562
|
+
if (vueInstance.__vue__) {
|
|
563
|
+
return vueInstance.__vue__;
|
|
564
|
+
}
|
|
565
|
+
let current = element.parentElement;
|
|
566
|
+
let depth = 0;
|
|
567
|
+
const maxDepth = 5;
|
|
568
|
+
while (current && depth < maxDepth) {
|
|
569
|
+
const parentVueInstance = current;
|
|
570
|
+
if (parentVueInstance.__vueParentComponent) {
|
|
571
|
+
return parentVueInstance.__vueParentComponent;
|
|
572
|
+
}
|
|
573
|
+
if (parentVueInstance.__vue__) {
|
|
574
|
+
return parentVueInstance.__vue__;
|
|
575
|
+
}
|
|
576
|
+
current = current.parentElement;
|
|
577
|
+
depth++;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
function getComponentName(instance) {
|
|
582
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
583
|
+
if (!instance) {
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
if ((_a = instance.$options) == null ? void 0 : _a.name) {
|
|
587
|
+
return instance.$options.name;
|
|
588
|
+
}
|
|
589
|
+
if (instance.__name) {
|
|
590
|
+
return instance.__name;
|
|
591
|
+
}
|
|
592
|
+
if ((_b = instance.type) == null ? void 0 : _b.name) {
|
|
593
|
+
return instance.type.name;
|
|
594
|
+
}
|
|
595
|
+
if ((_c = instance.type) == null ? void 0 : _c.__name) {
|
|
596
|
+
return instance.type.__name;
|
|
597
|
+
}
|
|
598
|
+
if ((_e = (_d = instance.$) == null ? void 0 : _d.type) == null ? void 0 : _e.name) {
|
|
599
|
+
return instance.$.type.name;
|
|
600
|
+
}
|
|
601
|
+
if ((_g = (_f = instance.$) == null ? void 0 : _f.type) == null ? void 0 : _g.__name) {
|
|
602
|
+
return instance.$.type.__name;
|
|
603
|
+
}
|
|
604
|
+
return void 0;
|
|
605
|
+
}
|
|
606
|
+
function getComponentProps(instance) {
|
|
607
|
+
if (!instance) {
|
|
608
|
+
return void 0;
|
|
609
|
+
}
|
|
610
|
+
if (instance.$props) {
|
|
611
|
+
return instance.$props;
|
|
612
|
+
}
|
|
613
|
+
if (instance.props) {
|
|
614
|
+
return instance.props;
|
|
615
|
+
}
|
|
616
|
+
return void 0;
|
|
617
|
+
}
|
|
618
|
+
function getComponentAttrs(instance) {
|
|
619
|
+
if (!instance) {
|
|
620
|
+
return void 0;
|
|
621
|
+
}
|
|
622
|
+
if (instance.$attrs) {
|
|
623
|
+
return instance.$attrs;
|
|
624
|
+
}
|
|
625
|
+
if (instance.attrs) {
|
|
626
|
+
return instance.attrs;
|
|
627
|
+
}
|
|
628
|
+
return void 0;
|
|
629
|
+
}
|
|
630
|
+
function getComponentValue(instance) {
|
|
631
|
+
if (!instance) {
|
|
632
|
+
return void 0;
|
|
633
|
+
}
|
|
634
|
+
const props = getComponentProps(instance);
|
|
635
|
+
if (props) {
|
|
636
|
+
if ("value" in props) {
|
|
637
|
+
return props.value;
|
|
638
|
+
}
|
|
639
|
+
if ("modelValue" in props) {
|
|
640
|
+
return props.modelValue;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (instance.setupState) {
|
|
644
|
+
if ("value" in instance.setupState) {
|
|
645
|
+
return instance.setupState.value;
|
|
646
|
+
}
|
|
647
|
+
if ("modelValue" in instance.setupState) {
|
|
648
|
+
return instance.setupState.modelValue;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (instance.ctx) {
|
|
652
|
+
if ("value" in instance.ctx) {
|
|
653
|
+
return instance.ctx.value;
|
|
654
|
+
}
|
|
655
|
+
if ("modelValue" in instance.ctx) {
|
|
656
|
+
return instance.ctx.modelValue;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return void 0;
|
|
660
|
+
}
|
|
661
|
+
const SENSITIVE_KEYS = ["password", "token", "secret", "key", "auth", "credential", "pwd"];
|
|
662
|
+
function filterSensitiveData(data) {
|
|
663
|
+
if (!data) {
|
|
664
|
+
return void 0;
|
|
665
|
+
}
|
|
666
|
+
const filtered = {};
|
|
667
|
+
for (const [key, value] of Object.entries(data)) {
|
|
668
|
+
const lowerKey = key.toLowerCase();
|
|
669
|
+
const isSensitive = SENSITIVE_KEYS.some((sensitiveKey) => {
|
|
670
|
+
const lowerSensitiveKey = sensitiveKey.toLowerCase();
|
|
671
|
+
if (lowerKey === lowerSensitiveKey) return true;
|
|
672
|
+
const index = lowerKey.indexOf(lowerSensitiveKey);
|
|
673
|
+
if (index < 0) return false;
|
|
674
|
+
const prevChar = index > 0 ? key[index - 1] : null;
|
|
675
|
+
const nextChar = index + lowerSensitiveKey.length < key.length ? key[index + lowerSensitiveKey.length] : null;
|
|
676
|
+
if (index === 0 && nextChar !== null) {
|
|
677
|
+
if (nextChar === "-" || nextChar === "_" || /[A-Z]/.test(nextChar)) {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (index === lowerKey.length - lowerSensitiveKey.length && prevChar !== null) {
|
|
682
|
+
if (prevChar === "-" || prevChar === "_" || /[A-Z]/.test(prevChar)) {
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
const originalSensitivePart = key.substring(index, index + lowerSensitiveKey.length);
|
|
686
|
+
if (/[A-Z]/.test(originalSensitivePart[0])) {
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (index > 0 && index < lowerKey.length - lowerSensitiveKey.length) {
|
|
691
|
+
if (prevChar !== null && nextChar !== null) {
|
|
692
|
+
if ((prevChar === "-" || prevChar === "_" || /[A-Z]/.test(prevChar)) && (nextChar === "-" || nextChar === "_" || /[A-Z]/.test(nextChar))) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
const originalSensitivePart = key.substring(index, index + lowerSensitiveKey.length);
|
|
696
|
+
if (/[A-Z]/.test(originalSensitivePart[0])) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return false;
|
|
702
|
+
});
|
|
703
|
+
if (isSensitive) {
|
|
704
|
+
if (value === null) {
|
|
705
|
+
filtered[key] = "[null]";
|
|
706
|
+
} else if (value === void 0) {
|
|
707
|
+
continue;
|
|
708
|
+
} else {
|
|
709
|
+
filtered[key] = `[${typeof value}]`;
|
|
710
|
+
}
|
|
711
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
712
|
+
const nestedFiltered = filterSensitiveData(value);
|
|
713
|
+
filtered[key] = nestedFiltered !== void 0 ? nestedFiltered : {};
|
|
714
|
+
} else {
|
|
715
|
+
filtered[key] = value;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return filtered;
|
|
719
|
+
}
|
|
720
|
+
function identifyNaiveUIComponent(element) {
|
|
721
|
+
var _a, _b;
|
|
722
|
+
if (!element) {
|
|
723
|
+
return void 0;
|
|
724
|
+
}
|
|
725
|
+
const className = element.className;
|
|
726
|
+
if (typeof className === "string") {
|
|
727
|
+
const match = className.match(/\bn-([a-z-]+)\b/);
|
|
728
|
+
if (match) {
|
|
729
|
+
return `n-${match[1]}`;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const tagName = (_a = element.tagName) == null ? void 0 : _a.toLowerCase();
|
|
733
|
+
if (tagName && tagName.startsWith("n-")) {
|
|
734
|
+
return tagName;
|
|
735
|
+
}
|
|
736
|
+
let current = element.parentElement;
|
|
737
|
+
let depth = 0;
|
|
738
|
+
const maxDepth = 3;
|
|
739
|
+
while (current && depth < maxDepth) {
|
|
740
|
+
const parentClassName = current.className;
|
|
741
|
+
if (typeof parentClassName === "string") {
|
|
742
|
+
const match = parentClassName.match(/\bn-([a-z-]+)\b/);
|
|
743
|
+
if (match) {
|
|
744
|
+
return `n-${match[1]}`;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
const parentTagName = (_b = current.tagName) == null ? void 0 : _b.toLowerCase();
|
|
748
|
+
if (parentTagName && parentTagName.startsWith("n-")) {
|
|
749
|
+
return parentTagName;
|
|
750
|
+
}
|
|
751
|
+
current = current.parentElement;
|
|
752
|
+
depth++;
|
|
753
|
+
}
|
|
754
|
+
return void 0;
|
|
755
|
+
}
|
|
756
|
+
function extractVueComponentInfo(element, options = {}) {
|
|
757
|
+
var _a;
|
|
758
|
+
if (!element || typeof window === "undefined") {
|
|
759
|
+
return void 0;
|
|
760
|
+
}
|
|
761
|
+
const {
|
|
762
|
+
extractProps = true,
|
|
763
|
+
extractAttrs = true,
|
|
764
|
+
extractValue = true,
|
|
765
|
+
extractLabel = true
|
|
766
|
+
} = options;
|
|
767
|
+
const naiveUIComponentName = identifyNaiveUIComponent(element);
|
|
768
|
+
const vueInstance = getVueComponentInstance(element);
|
|
769
|
+
const componentName = naiveUIComponentName || getComponentName(vueInstance);
|
|
770
|
+
if (!componentName && !vueInstance) {
|
|
771
|
+
return void 0;
|
|
772
|
+
}
|
|
773
|
+
const componentInfo = {};
|
|
774
|
+
if (componentName) {
|
|
775
|
+
componentInfo.componentName = componentName;
|
|
776
|
+
}
|
|
777
|
+
if (extractProps && vueInstance) {
|
|
778
|
+
const props = getComponentProps(vueInstance);
|
|
779
|
+
if (props) {
|
|
780
|
+
componentInfo.props = filterSensitiveData(props);
|
|
781
|
+
if ("type" in props) {
|
|
782
|
+
componentInfo.componentType = String(props.type);
|
|
783
|
+
}
|
|
784
|
+
if ("disabled" in props) {
|
|
785
|
+
componentInfo.disabled = Boolean(props.disabled);
|
|
786
|
+
}
|
|
787
|
+
if ("label" in props && extractLabel) {
|
|
788
|
+
componentInfo.label = String(props.label);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
if (extractAttrs && vueInstance) {
|
|
793
|
+
const attrs = getComponentAttrs(vueInstance);
|
|
794
|
+
if (attrs) {
|
|
795
|
+
componentInfo.attrs = filterSensitiveData(attrs);
|
|
796
|
+
if (!componentInfo.componentType && "type" in attrs) {
|
|
797
|
+
componentInfo.componentType = String(attrs.type);
|
|
798
|
+
}
|
|
799
|
+
if (componentInfo.disabled === void 0 && "disabled" in attrs) {
|
|
800
|
+
componentInfo.disabled = Boolean(attrs.disabled);
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (extractValue && vueInstance) {
|
|
805
|
+
const value = getComponentValue(vueInstance);
|
|
806
|
+
if (value !== void 0 && value !== null) {
|
|
807
|
+
const props = getComponentProps(vueInstance);
|
|
808
|
+
const isSensitive = props && ("type" in props && String(props.type).toLowerCase() === "password" || "password" in props || componentName === "n-input" && "type" in props && String(props.type).toLowerCase() === "password");
|
|
809
|
+
if (isSensitive) {
|
|
810
|
+
const valueLength = typeof value === "string" ? value.length : void 0;
|
|
811
|
+
componentInfo.value = valueLength !== void 0 ? `[password:${valueLength} chars]` : `[${typeof value}]`;
|
|
812
|
+
} else {
|
|
813
|
+
if (typeof value === "object") {
|
|
814
|
+
if (Array.isArray(value)) {
|
|
815
|
+
componentInfo.value = `[Array(${value.length})]`;
|
|
816
|
+
if (componentName === "n-select" || componentName === "n-cascader") {
|
|
817
|
+
componentInfo.options = {
|
|
818
|
+
count: value.length,
|
|
819
|
+
multiple: props && "multiple" in props ? Boolean(props.multiple) : false
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
componentInfo.value = `[Object]`;
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
componentInfo.value = value;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
if (extractLabel && !componentInfo.label) {
|
|
832
|
+
const text = (_a = element.textContent) == null ? void 0 : _a.trim();
|
|
833
|
+
if (text && text.length > 0 && text.length < 100) {
|
|
834
|
+
componentInfo.label = text.substring(0, 100);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if ((componentName === "n-select" || componentName === "n-cascader") && vueInstance) {
|
|
838
|
+
const props = getComponentProps(vueInstance);
|
|
839
|
+
if (props && "options" in props && Array.isArray(props.options)) {
|
|
840
|
+
componentInfo.options = {
|
|
841
|
+
count: props.options.length,
|
|
842
|
+
multiple: props && "multiple" in props ? Boolean(props.multiple) : false
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return Object.keys(componentInfo).length > 0 ? componentInfo : void 0;
|
|
847
|
+
}
|
|
848
|
+
function isVueComponent(element) {
|
|
849
|
+
if (!element) {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
const vueInstance = getVueComponentInstance(element);
|
|
853
|
+
return vueInstance !== null;
|
|
854
|
+
}
|
|
855
|
+
function isNaiveUIComponent(element) {
|
|
856
|
+
if (!element) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
const componentName = identifyNaiveUIComponent(element);
|
|
860
|
+
return componentName !== void 0 && componentName.startsWith("n-");
|
|
861
|
+
}
|
|
862
|
+
const NAIVE_UI_COMPONENT_TYPES = {
|
|
863
|
+
"n-button": ["default", "primary", "success", "info", "warning", "error", "tertiary", "quaternary"],
|
|
864
|
+
"n-input": ["text", "password", "textarea", "number", "email", "tel", "url"],
|
|
865
|
+
"n-select": ["default", "multiple", "tags", "filterable"],
|
|
866
|
+
"n-checkbox": ["default", "indeterminate"],
|
|
867
|
+
"n-radio": ["default", "button"],
|
|
868
|
+
"n-switch": ["default"],
|
|
869
|
+
"n-date-picker": ["date", "datetime", "daterange", "datetimerange"],
|
|
870
|
+
"n-time-picker": ["time", "timerange"],
|
|
871
|
+
"n-cascader": ["default", "multiple", "filterable"],
|
|
872
|
+
"n-transfer": ["default"],
|
|
873
|
+
"n-tree-select": ["default", "multiple", "filterable"],
|
|
874
|
+
"n-form": ["default", "inline"],
|
|
875
|
+
"n-form-item": ["default"],
|
|
876
|
+
"n-table": ["default", "bordered", "striped"],
|
|
877
|
+
"n-pagination": ["default"],
|
|
878
|
+
"n-menu": ["default", "horizontal", "vertical"],
|
|
879
|
+
"n-tabs": ["default", "card", "bar"],
|
|
880
|
+
"n-drawer": ["default"],
|
|
881
|
+
"n-modal": ["default"],
|
|
882
|
+
"n-message": ["default", "success", "info", "warning", "error"],
|
|
883
|
+
"n-notification": ["default", "success", "info", "warning", "error"],
|
|
884
|
+
"n-popover": ["default"],
|
|
885
|
+
"n-tooltip": ["default"],
|
|
886
|
+
"n-dropdown": ["default"],
|
|
887
|
+
"n-breadcrumb": ["default"],
|
|
888
|
+
"n-steps": ["default", "vertical"],
|
|
889
|
+
"n-timeline": ["default"],
|
|
890
|
+
"n-card": ["default", "bordered", "hoverable"],
|
|
891
|
+
"n-collapse": ["default", "accordion"],
|
|
892
|
+
"n-divider": ["default", "horizontal", "vertical"],
|
|
893
|
+
"n-space": ["default", "vertical"],
|
|
894
|
+
"n-grid": ["default"],
|
|
895
|
+
"n-layout": ["default"],
|
|
896
|
+
"n-back-top": ["default"],
|
|
897
|
+
"n-anchor": ["default"],
|
|
898
|
+
"n-affix": ["default"],
|
|
899
|
+
"n-watermark": ["default"],
|
|
900
|
+
"n-spin": ["default"],
|
|
901
|
+
"n-skeleton": ["default"],
|
|
902
|
+
"n-progress": ["default", "line", "circle", "dashboard"],
|
|
903
|
+
"n-rate": ["default"],
|
|
904
|
+
"n-slider": ["default", "range"],
|
|
905
|
+
"n-color-picker": ["default"],
|
|
906
|
+
"n-image": ["default"],
|
|
907
|
+
"n-avatar": ["default", "circle", "square"],
|
|
908
|
+
"n-badge": ["default"],
|
|
909
|
+
"n-tag": ["default", "success", "info", "warning", "error"],
|
|
910
|
+
"n-empty": ["default"],
|
|
911
|
+
"n-result": ["default", "success", "info", "warning", "error", "404", "500"],
|
|
912
|
+
"n-descriptions": ["default", "bordered"],
|
|
913
|
+
"n-statistic": ["default"],
|
|
914
|
+
"n-list": ["default", "bordered"],
|
|
915
|
+
"n-list-item": ["default"],
|
|
916
|
+
"n-carousel": ["default", "dot", "arrow"],
|
|
917
|
+
"n-calendar": ["default"]
|
|
918
|
+
};
|
|
919
|
+
function extractNaiveUIComponentInfo(element, options = {}) {
|
|
920
|
+
if (!element) {
|
|
921
|
+
return void 0;
|
|
922
|
+
}
|
|
923
|
+
if (!isNaiveUIComponent(element)) {
|
|
924
|
+
return void 0;
|
|
925
|
+
}
|
|
926
|
+
const componentInfo = extractVueComponentInfo(element, {
|
|
927
|
+
extractProps: options.extractProps,
|
|
928
|
+
extractAttrs: options.extractAttrs,
|
|
929
|
+
extractValue: options.extractValue,
|
|
930
|
+
extractLabel: options.extractLabel
|
|
931
|
+
});
|
|
932
|
+
if (!componentInfo) {
|
|
933
|
+
return void 0;
|
|
934
|
+
}
|
|
935
|
+
const componentName = componentInfo.componentName;
|
|
936
|
+
if (!componentName || !componentName.startsWith("n-")) {
|
|
937
|
+
return componentInfo;
|
|
938
|
+
}
|
|
939
|
+
switch (componentName) {
|
|
940
|
+
case "n-button":
|
|
941
|
+
enhanceButtonInfo(element, componentInfo);
|
|
942
|
+
break;
|
|
943
|
+
case "n-input":
|
|
944
|
+
enhanceInputInfo(element, componentInfo);
|
|
945
|
+
break;
|
|
946
|
+
case "n-select":
|
|
947
|
+
case "n-cascader":
|
|
948
|
+
case "n-tree-select":
|
|
949
|
+
enhanceSelectInfo(element, componentInfo);
|
|
950
|
+
break;
|
|
951
|
+
case "n-checkbox":
|
|
952
|
+
case "n-radio":
|
|
953
|
+
enhanceCheckboxRadioInfo(element, componentInfo);
|
|
954
|
+
break;
|
|
955
|
+
case "n-switch":
|
|
956
|
+
enhanceSwitchInfo(element, componentInfo);
|
|
957
|
+
break;
|
|
958
|
+
case "n-form":
|
|
959
|
+
case "n-form-item":
|
|
960
|
+
enhanceFormInfo(element, componentInfo);
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
return componentInfo;
|
|
964
|
+
}
|
|
965
|
+
function enhanceButtonInfo(element, componentInfo) {
|
|
966
|
+
var _a;
|
|
967
|
+
if (!componentInfo.label) {
|
|
968
|
+
const text = (_a = element.textContent) == null ? void 0 : _a.trim();
|
|
969
|
+
if (text) {
|
|
970
|
+
componentInfo.label = text.substring(0, 50);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
const iconElement = element.querySelector('.n-button__icon, [class*="icon"]');
|
|
974
|
+
if (iconElement) {
|
|
975
|
+
componentInfo.hasIcon = true;
|
|
976
|
+
}
|
|
977
|
+
if (element.classList.contains("n-button--disabled") || element.hasAttribute("disabled")) {
|
|
978
|
+
componentInfo.disabled = true;
|
|
979
|
+
}
|
|
980
|
+
if (element.classList.contains("n-button--loading")) {
|
|
981
|
+
componentInfo.loading = true;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function enhanceInputInfo(element, componentInfo) {
|
|
985
|
+
var _a;
|
|
986
|
+
const inputElement = element.querySelector("input, textarea");
|
|
987
|
+
if (inputElement) {
|
|
988
|
+
if (!((_a = componentInfo.attrs) == null ? void 0 : _a.placeholder) && inputElement.placeholder) {
|
|
989
|
+
if (!componentInfo.attrs) {
|
|
990
|
+
componentInfo.attrs = {};
|
|
991
|
+
}
|
|
992
|
+
componentInfo.attrs.placeholder = inputElement.placeholder;
|
|
993
|
+
}
|
|
994
|
+
if (inputElement.tagName.toLowerCase() === "input") {
|
|
995
|
+
const inputType = inputElement.type || "text";
|
|
996
|
+
if (!componentInfo.componentType) {
|
|
997
|
+
componentInfo.componentType = inputType;
|
|
998
|
+
}
|
|
999
|
+
if (inputType === "password") {
|
|
1000
|
+
componentInfo.isSensitive = true;
|
|
1001
|
+
if (componentInfo.value && typeof componentInfo.value === "string") {
|
|
1002
|
+
if (!componentInfo.value.startsWith("[password:")) {
|
|
1003
|
+
componentInfo.value = `[password:${componentInfo.value.length} chars]`;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const value = inputElement.value;
|
|
1009
|
+
if (value) {
|
|
1010
|
+
const isPassword = inputElement.type === "password" || componentInfo.props && "type" in componentInfo.props && String(componentInfo.props.type).toLowerCase() === "password";
|
|
1011
|
+
if (isPassword) {
|
|
1012
|
+
componentInfo.value = `[password:${value.length} chars]`;
|
|
1013
|
+
componentInfo.isSensitive = true;
|
|
1014
|
+
} else {
|
|
1015
|
+
componentInfo.value = value.length > 100 ? value.substring(0, 100) + "..." : value;
|
|
1016
|
+
}
|
|
1017
|
+
} else if (!componentInfo.value) ;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function enhanceSelectInfo(element, componentInfo) {
|
|
1021
|
+
var _a;
|
|
1022
|
+
const selectedElement = element.querySelector(".n-base-selection, .n-base-selection-label");
|
|
1023
|
+
if (selectedElement) {
|
|
1024
|
+
const selectedText = (_a = selectedElement.textContent) == null ? void 0 : _a.trim();
|
|
1025
|
+
if (selectedText && !componentInfo.label) {
|
|
1026
|
+
componentInfo.label = selectedText.substring(0, 100);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (element.classList.contains("n-base-selection--multiple")) {
|
|
1030
|
+
if (!componentInfo.options) {
|
|
1031
|
+
componentInfo.options = {};
|
|
1032
|
+
}
|
|
1033
|
+
componentInfo.options.multiple = true;
|
|
1034
|
+
}
|
|
1035
|
+
if (element.querySelector(".n-base-selection-input")) {
|
|
1036
|
+
componentInfo.filterable = true;
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function enhanceCheckboxRadioInfo(element, componentInfo) {
|
|
1040
|
+
var _a;
|
|
1041
|
+
const inputElement = element.querySelector('input[type="checkbox"], input[type="radio"]');
|
|
1042
|
+
if (inputElement) {
|
|
1043
|
+
componentInfo.checked = inputElement.checked;
|
|
1044
|
+
const labelElement = element.querySelector("label, .n-checkbox__label, .n-radio__label");
|
|
1045
|
+
if (labelElement) {
|
|
1046
|
+
const labelText = (_a = labelElement.textContent) == null ? void 0 : _a.trim();
|
|
1047
|
+
if (labelText && !componentInfo.label) {
|
|
1048
|
+
componentInfo.label = labelText.substring(0, 100);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (inputElement.value) {
|
|
1052
|
+
componentInfo.value = inputElement.value;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function enhanceSwitchInfo(element, componentInfo) {
|
|
1057
|
+
const inputElement = element.querySelector('input[type="checkbox"]');
|
|
1058
|
+
if (inputElement) {
|
|
1059
|
+
componentInfo.checked = inputElement.checked;
|
|
1060
|
+
componentInfo.value = inputElement.checked;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function enhanceFormInfo(element, componentInfo) {
|
|
1064
|
+
var _a;
|
|
1065
|
+
const labelElement = element.querySelector(".n-form-item-label, label");
|
|
1066
|
+
if (labelElement) {
|
|
1067
|
+
const labelText = (_a = labelElement.textContent) == null ? void 0 : _a.trim();
|
|
1068
|
+
if (labelText && !componentInfo.label) {
|
|
1069
|
+
componentInfo.label = labelText.substring(0, 100);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const requiredElement = element.querySelector('.n-form-item-label__asterisk, [class*="required"]');
|
|
1073
|
+
if (requiredElement) {
|
|
1074
|
+
componentInfo.required = true;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function isNaiveUIComponentType(element, componentType) {
|
|
1078
|
+
if (!element) {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
const componentName = identifyNaiveUIComponent(element);
|
|
1082
|
+
return componentName === componentType;
|
|
1083
|
+
}
|
|
1084
|
+
function getNaiveUIComponentTypes(componentName) {
|
|
1085
|
+
return NAIVE_UI_COMPONENT_TYPES[componentName];
|
|
1086
|
+
}
|
|
507
1087
|
class ClickTracker {
|
|
508
1088
|
constructor() {
|
|
509
1089
|
__publicField(this, "clickHandler", null);
|
|
@@ -515,18 +1095,28 @@ class ClickTracker {
|
|
|
515
1095
|
__publicField(this, "TOUCH_CLICK_THRESHOLD", 300);
|
|
516
1096
|
// ms
|
|
517
1097
|
__publicField(this, "TOUCH_DISTANCE_THRESHOLD", 10);
|
|
1098
|
+
// px
|
|
1099
|
+
__publicField(this, "enableVueTracking", false);
|
|
1100
|
+
__publicField(this, "enableNaiveUITracking", false);
|
|
1101
|
+
__publicField(this, "extractComponentProps", true);
|
|
1102
|
+
__publicField(this, "extractComponentValue", true);
|
|
518
1103
|
}
|
|
519
|
-
// px
|
|
520
1104
|
/**
|
|
521
1105
|
* 初始化
|
|
522
1106
|
*/
|
|
523
|
-
init(debounceTime) {
|
|
1107
|
+
init(debounceTime, options) {
|
|
524
1108
|
if (typeof window === "undefined") {
|
|
525
1109
|
return;
|
|
526
1110
|
}
|
|
527
1111
|
if (debounceTime !== void 0) {
|
|
528
1112
|
this.debounceTime = debounceTime;
|
|
529
1113
|
}
|
|
1114
|
+
if (options) {
|
|
1115
|
+
this.enableVueTracking = options.enableVueTracking ?? false;
|
|
1116
|
+
this.enableNaiveUITracking = options.enableNaiveUITracking ?? false;
|
|
1117
|
+
this.extractComponentProps = options.extractComponentProps ?? true;
|
|
1118
|
+
this.extractComponentValue = options.extractComponentValue ?? true;
|
|
1119
|
+
}
|
|
530
1120
|
this.clickHandler = debounce((...args) => {
|
|
531
1121
|
const event = args[0];
|
|
532
1122
|
this.handleClick(event);
|
|
@@ -590,6 +1180,9 @@ class ClickTracker {
|
|
|
590
1180
|
* 处理触摸交互(模拟点击行为)
|
|
591
1181
|
*/
|
|
592
1182
|
processTouchInteraction(touch, element) {
|
|
1183
|
+
if (isTrackerInternalPanel()) {
|
|
1184
|
+
return;
|
|
1185
|
+
}
|
|
593
1186
|
if (!element) {
|
|
594
1187
|
return;
|
|
595
1188
|
}
|
|
@@ -617,6 +1210,30 @@ class ClickTracker {
|
|
|
617
1210
|
interactionType: "touch"
|
|
618
1211
|
// 标记为触摸交互
|
|
619
1212
|
};
|
|
1213
|
+
if (this.enableNaiveUITracking || this.enableVueTracking) {
|
|
1214
|
+
try {
|
|
1215
|
+
if (this.enableNaiveUITracking) {
|
|
1216
|
+
const naiveUIInfo = extractNaiveUIComponentInfo(element, {
|
|
1217
|
+
extractProps: this.extractComponentProps,
|
|
1218
|
+
extractValue: this.extractComponentValue
|
|
1219
|
+
});
|
|
1220
|
+
if (naiveUIInfo) {
|
|
1221
|
+
touchEvent.componentInfo = naiveUIInfo;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
if (!touchEvent.componentInfo && this.enableVueTracking) {
|
|
1225
|
+
const vueInfo = extractVueComponentInfo(element, {
|
|
1226
|
+
extractProps: this.extractComponentProps,
|
|
1227
|
+
extractValue: this.extractComponentValue
|
|
1228
|
+
});
|
|
1229
|
+
if (vueInfo) {
|
|
1230
|
+
touchEvent.componentInfo = vueInfo;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
} catch (componentError) {
|
|
1234
|
+
console.warn("ClickTracker: Error extracting component info:", componentError);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
620
1237
|
if (typeof window !== "undefined" && window.__trackerEventBus) {
|
|
621
1238
|
window.__trackerEventBus.emit("click", touchEvent);
|
|
622
1239
|
}
|
|
@@ -634,6 +1251,9 @@ class ClickTracker {
|
|
|
634
1251
|
* 处理点击事件
|
|
635
1252
|
*/
|
|
636
1253
|
handleClick(event) {
|
|
1254
|
+
if (isTrackerInternalPanel()) {
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
637
1257
|
try {
|
|
638
1258
|
const target = event.target;
|
|
639
1259
|
if (!target) {
|
|
@@ -662,6 +1282,30 @@ class ClickTracker {
|
|
|
662
1282
|
interactionType: "mouse"
|
|
663
1283
|
// 显式标识为鼠标交互
|
|
664
1284
|
};
|
|
1285
|
+
if (this.enableNaiveUITracking || this.enableVueTracking) {
|
|
1286
|
+
try {
|
|
1287
|
+
if (this.enableNaiveUITracking) {
|
|
1288
|
+
const naiveUIInfo = extractNaiveUIComponentInfo(target, {
|
|
1289
|
+
extractProps: this.extractComponentProps,
|
|
1290
|
+
extractValue: this.extractComponentValue
|
|
1291
|
+
});
|
|
1292
|
+
if (naiveUIInfo) {
|
|
1293
|
+
clickEvent.componentInfo = naiveUIInfo;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (!clickEvent.componentInfo && this.enableVueTracking) {
|
|
1297
|
+
const vueInfo = extractVueComponentInfo(target, {
|
|
1298
|
+
extractProps: this.extractComponentProps,
|
|
1299
|
+
extractValue: this.extractComponentValue
|
|
1300
|
+
});
|
|
1301
|
+
if (vueInfo) {
|
|
1302
|
+
clickEvent.componentInfo = vueInfo;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
} catch (componentError) {
|
|
1306
|
+
console.warn("ClickTracker: Error extracting component info:", componentError);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
665
1309
|
if (typeof window !== "undefined" && window.__trackerEventBus) {
|
|
666
1310
|
window.__trackerEventBus.emit("click", clickEvent);
|
|
667
1311
|
}
|
|
@@ -821,6 +1465,9 @@ class ExposureTracker {
|
|
|
821
1465
|
* 处理IntersectionObserver回调
|
|
822
1466
|
*/
|
|
823
1467
|
handleIntersection(entry) {
|
|
1468
|
+
if (isTrackerInternalPanel()) {
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
824
1471
|
const { target, intersectionRatio, isIntersecting } = entry;
|
|
825
1472
|
if (isIntersecting && intersectionRatio >= (this.options.threshold || 0.5)) {
|
|
826
1473
|
if (!this.exposedElements.has(target)) {
|
|
@@ -955,11 +1602,15 @@ class InputTracker {
|
|
|
955
1602
|
// 默认跳过空输入
|
|
956
1603
|
// 敏感字段关键词列表
|
|
957
1604
|
__publicField(this, "sensitiveKeywords", ["password", "pwd", "secret", "token", "key", "auth", "credential"]);
|
|
1605
|
+
__publicField(this, "enableVueTracking", false);
|
|
1606
|
+
__publicField(this, "enableNaiveUITracking", false);
|
|
1607
|
+
__publicField(this, "extractComponentProps", true);
|
|
1608
|
+
__publicField(this, "extractComponentValue", true);
|
|
958
1609
|
}
|
|
959
1610
|
/**
|
|
960
1611
|
* 初始化
|
|
961
1612
|
*/
|
|
962
|
-
init(debounceTime, skipEmptyInput) {
|
|
1613
|
+
init(debounceTime, skipEmptyInput, options) {
|
|
963
1614
|
if (typeof window === "undefined") {
|
|
964
1615
|
return;
|
|
965
1616
|
}
|
|
@@ -970,6 +1621,12 @@ class InputTracker {
|
|
|
970
1621
|
if (skipEmptyInput !== void 0) {
|
|
971
1622
|
this.skipEmptyInput = skipEmptyInput;
|
|
972
1623
|
}
|
|
1624
|
+
if (options) {
|
|
1625
|
+
this.enableVueTracking = options.enableVueTracking ?? false;
|
|
1626
|
+
this.enableNaiveUITracking = options.enableNaiveUITracking ?? false;
|
|
1627
|
+
this.extractComponentProps = options.extractComponentProps ?? true;
|
|
1628
|
+
this.extractComponentValue = options.extractComponentValue ?? true;
|
|
1629
|
+
}
|
|
973
1630
|
this.inputHandler = debounce((...args) => {
|
|
974
1631
|
const event = args[0];
|
|
975
1632
|
this.handleInput(event);
|
|
@@ -986,6 +1643,9 @@ class InputTracker {
|
|
|
986
1643
|
*/
|
|
987
1644
|
handleInput(event) {
|
|
988
1645
|
var _a;
|
|
1646
|
+
if (isTrackerInternalPanel()) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
989
1649
|
const target = event.target;
|
|
990
1650
|
if (!target) {
|
|
991
1651
|
return;
|
|
@@ -1047,6 +1707,48 @@ class InputTracker {
|
|
|
1047
1707
|
valueLength,
|
|
1048
1708
|
isSensitive
|
|
1049
1709
|
};
|
|
1710
|
+
if (this.enableNaiveUITracking || this.enableVueTracking) {
|
|
1711
|
+
try {
|
|
1712
|
+
if (this.enableNaiveUITracking) {
|
|
1713
|
+
const naiveUIInfo = extractNaiveUIComponentInfo(target, {
|
|
1714
|
+
extractProps: this.extractComponentProps,
|
|
1715
|
+
extractValue: this.extractComponentValue
|
|
1716
|
+
});
|
|
1717
|
+
if (naiveUIInfo) {
|
|
1718
|
+
inputEvent.componentInfo = naiveUIInfo;
|
|
1719
|
+
if (naiveUIInfo.value !== void 0 && !isSensitive) {
|
|
1720
|
+
if (typeof naiveUIInfo.value === "string") {
|
|
1721
|
+
if (!naiveUIInfo.value.startsWith("[password:") && !naiveUIInfo.value.startsWith("[FILTERED]")) {
|
|
1722
|
+
inputEvent.value = naiveUIInfo.value;
|
|
1723
|
+
}
|
|
1724
|
+
} else if (typeof naiveUIInfo.value === "number" || typeof naiveUIInfo.value === "boolean") {
|
|
1725
|
+
inputEvent.value = String(naiveUIInfo.value);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
if (!inputEvent.componentInfo && this.enableVueTracking) {
|
|
1731
|
+
const vueInfo = extractVueComponentInfo(target, {
|
|
1732
|
+
extractProps: this.extractComponentProps,
|
|
1733
|
+
extractValue: this.extractComponentValue
|
|
1734
|
+
});
|
|
1735
|
+
if (vueInfo) {
|
|
1736
|
+
inputEvent.componentInfo = vueInfo;
|
|
1737
|
+
if (vueInfo.value !== void 0 && !isSensitive) {
|
|
1738
|
+
if (typeof vueInfo.value === "string") {
|
|
1739
|
+
if (!vueInfo.value.startsWith("[password:") && !vueInfo.value.startsWith("[FILTERED]")) {
|
|
1740
|
+
inputEvent.value = vueInfo.value;
|
|
1741
|
+
}
|
|
1742
|
+
} else if (typeof vueInfo.value === "number" || typeof vueInfo.value === "boolean") {
|
|
1743
|
+
inputEvent.value = String(vueInfo.value);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
} catch (componentError) {
|
|
1749
|
+
console.warn("InputTracker: Error extracting component info:", componentError);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1050
1752
|
if (typeof window !== "undefined" && window.__trackerEventBus) {
|
|
1051
1753
|
window.__trackerEventBus.emit("input", inputEvent);
|
|
1052
1754
|
}
|
|
@@ -1065,6 +1767,9 @@ class InputTracker {
|
|
|
1065
1767
|
*/
|
|
1066
1768
|
handleChange(event) {
|
|
1067
1769
|
var _a;
|
|
1770
|
+
if (isTrackerInternalPanel()) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1068
1773
|
const target = event.target;
|
|
1069
1774
|
if (!target) {
|
|
1070
1775
|
return;
|
|
@@ -1111,17 +1816,59 @@ class InputTracker {
|
|
|
1111
1816
|
valueLength,
|
|
1112
1817
|
isSensitive
|
|
1113
1818
|
};
|
|
1114
|
-
if (
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1819
|
+
if (this.enableNaiveUITracking || this.enableVueTracking) {
|
|
1820
|
+
try {
|
|
1821
|
+
if (this.enableNaiveUITracking) {
|
|
1822
|
+
const naiveUIInfo = extractNaiveUIComponentInfo(target, {
|
|
1823
|
+
extractProps: this.extractComponentProps,
|
|
1824
|
+
extractValue: this.extractComponentValue
|
|
1825
|
+
});
|
|
1826
|
+
if (naiveUIInfo) {
|
|
1827
|
+
inputEvent.componentInfo = naiveUIInfo;
|
|
1828
|
+
if (naiveUIInfo.value !== void 0 && !isSensitive) {
|
|
1829
|
+
if (typeof naiveUIInfo.value === "string") {
|
|
1830
|
+
if (!naiveUIInfo.value.startsWith("[password:") && !naiveUIInfo.value.startsWith("[FILTERED]")) {
|
|
1831
|
+
inputEvent.value = naiveUIInfo.value;
|
|
1832
|
+
}
|
|
1833
|
+
} else if (typeof naiveUIInfo.value === "number" || typeof naiveUIInfo.value === "boolean") {
|
|
1834
|
+
inputEvent.value = String(naiveUIInfo.value);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
if (!inputEvent.componentInfo && this.enableVueTracking) {
|
|
1840
|
+
const vueInfo = extractVueComponentInfo(target, {
|
|
1841
|
+
extractProps: this.extractComponentProps,
|
|
1842
|
+
extractValue: this.extractComponentValue
|
|
1843
|
+
});
|
|
1844
|
+
if (vueInfo) {
|
|
1845
|
+
inputEvent.componentInfo = vueInfo;
|
|
1846
|
+
if (vueInfo.value !== void 0 && !isSensitive) {
|
|
1847
|
+
if (typeof vueInfo.value === "string") {
|
|
1848
|
+
if (!vueInfo.value.startsWith("[password:") && !vueInfo.value.startsWith("[FILTERED]")) {
|
|
1849
|
+
inputEvent.value = vueInfo.value;
|
|
1850
|
+
}
|
|
1851
|
+
} else if (typeof vueInfo.value === "number" || typeof vueInfo.value === "boolean") {
|
|
1852
|
+
inputEvent.value = String(vueInfo.value);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
} catch (componentError) {
|
|
1858
|
+
console.warn("InputTracker: Error extracting component info:", componentError);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (typeof window !== "undefined" && window.__trackerEventBus) {
|
|
1862
|
+
window.__trackerEventBus.emit("input", inputEvent);
|
|
1863
|
+
}
|
|
1864
|
+
} catch (error) {
|
|
1865
|
+
console.error("InputTracker: Error processing change event:", error);
|
|
1866
|
+
}
|
|
1867
|
+
};
|
|
1868
|
+
if (typeof requestIdleCallback !== "undefined") {
|
|
1869
|
+
requestIdleCallback(processChange, { timeout: 100 });
|
|
1870
|
+
} else {
|
|
1871
|
+
setTimeout(processChange, 0);
|
|
1125
1872
|
}
|
|
1126
1873
|
}
|
|
1127
1874
|
/**
|
|
@@ -1194,16 +1941,36 @@ class AutoTracker {
|
|
|
1194
1941
|
if (this.enabled) {
|
|
1195
1942
|
return;
|
|
1196
1943
|
}
|
|
1197
|
-
const {
|
|
1944
|
+
const {
|
|
1945
|
+
enableClick = true,
|
|
1946
|
+
enableExposure = true,
|
|
1947
|
+
enableInput = true,
|
|
1948
|
+
clickDebounceTime,
|
|
1949
|
+
inputDebounceTime,
|
|
1950
|
+
enableVueTracking = false,
|
|
1951
|
+
enableNaiveUITracking = false,
|
|
1952
|
+
extractComponentProps = true,
|
|
1953
|
+
extractComponentValue = true
|
|
1954
|
+
} = options || {};
|
|
1198
1955
|
this.viewTracker.init();
|
|
1199
1956
|
if (enableClick) {
|
|
1200
|
-
this.clickTracker.init(clickDebounceTime
|
|
1957
|
+
this.clickTracker.init(clickDebounceTime, {
|
|
1958
|
+
enableVueTracking,
|
|
1959
|
+
enableNaiveUITracking,
|
|
1960
|
+
extractComponentProps,
|
|
1961
|
+
extractComponentValue
|
|
1962
|
+
});
|
|
1201
1963
|
}
|
|
1202
1964
|
if (enableExposure) {
|
|
1203
1965
|
this.exposureTracker.init();
|
|
1204
1966
|
}
|
|
1205
1967
|
if (enableInput) {
|
|
1206
|
-
this.inputTracker.init(inputDebounceTime
|
|
1968
|
+
this.inputTracker.init(inputDebounceTime, void 0, {
|
|
1969
|
+
enableVueTracking,
|
|
1970
|
+
enableNaiveUITracking,
|
|
1971
|
+
extractComponentProps,
|
|
1972
|
+
extractComponentValue
|
|
1973
|
+
});
|
|
1207
1974
|
}
|
|
1208
1975
|
this.enabled = true;
|
|
1209
1976
|
}
|
|
@@ -1307,6 +2074,9 @@ class ErrorTracker {
|
|
|
1307
2074
|
* 处理Promise错误
|
|
1308
2075
|
*/
|
|
1309
2076
|
handlePromiseError(event) {
|
|
2077
|
+
if (isTrackerInternalPanel()) {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
1310
2080
|
try {
|
|
1311
2081
|
const reason = event.reason;
|
|
1312
2082
|
let message = "Unhandled promise rejection";
|
|
@@ -1339,6 +2109,9 @@ class ErrorTracker {
|
|
|
1339
2109
|
*/
|
|
1340
2110
|
handleResourceError(event) {
|
|
1341
2111
|
var _a;
|
|
2112
|
+
if (isTrackerInternalPanel()) {
|
|
2113
|
+
return;
|
|
2114
|
+
}
|
|
1342
2115
|
try {
|
|
1343
2116
|
const target = event.target;
|
|
1344
2117
|
if (!target) {
|
|
@@ -1499,6 +2272,9 @@ class PerformanceTracker {
|
|
|
1499
2272
|
* 采集Navigation Timing数据
|
|
1500
2273
|
*/
|
|
1501
2274
|
collectNavigationTiming() {
|
|
2275
|
+
if (isTrackerInternalPanel()) {
|
|
2276
|
+
return;
|
|
2277
|
+
}
|
|
1502
2278
|
if (this.navigationTimingCollected) {
|
|
1503
2279
|
return;
|
|
1504
2280
|
}
|
|
@@ -1904,7 +2680,8 @@ class Deduplicator {
|
|
|
1904
2680
|
* 清理过期缓存
|
|
1905
2681
|
*/
|
|
1906
2682
|
cleanExpiredCache(now) {
|
|
1907
|
-
const
|
|
2683
|
+
const timeoutValues = Object.values(this.cacheTimeouts);
|
|
2684
|
+
const maxTimeout = timeoutValues.length > 0 ? Math.max(...timeoutValues, this.defaultCacheTimeout) : this.defaultCacheTimeout;
|
|
1908
2685
|
const expireTime = maxTimeout * 2;
|
|
1909
2686
|
for (const [key, time] of this.eventCache.entries()) {
|
|
1910
2687
|
if (now - time > expireTime) {
|
|
@@ -6243,6 +7020,24 @@ async function fetchPolyfill(url, init) {
|
|
|
6243
7020
|
reject(new DOMException("Aborted", "AbortError"));
|
|
6244
7021
|
return;
|
|
6245
7022
|
}
|
|
7023
|
+
if (typeof signal.addEventListener === "function") {
|
|
7024
|
+
signal.addEventListener("abort", () => {
|
|
7025
|
+
xhr.abort();
|
|
7026
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
7027
|
+
});
|
|
7028
|
+
} else {
|
|
7029
|
+
const checkAborted = setInterval(() => {
|
|
7030
|
+
if (signal.aborted) {
|
|
7031
|
+
clearInterval(checkAborted);
|
|
7032
|
+
xhr.abort();
|
|
7033
|
+
reject(new DOMException("Aborted", "AbortError"));
|
|
7034
|
+
}
|
|
7035
|
+
}, 100);
|
|
7036
|
+
const cleanup = () => clearInterval(checkAborted);
|
|
7037
|
+
xhr.addEventListener("loadend", cleanup);
|
|
7038
|
+
xhr.addEventListener("error", cleanup);
|
|
7039
|
+
xhr.addEventListener("timeout", cleanup);
|
|
7040
|
+
}
|
|
6246
7041
|
}
|
|
6247
7042
|
xhr.onload = () => {
|
|
6248
7043
|
const headersString = xhr.getAllResponseHeaders();
|
|
@@ -6311,15 +7106,18 @@ function getFetch() {
|
|
|
6311
7106
|
new (getAbortController())();
|
|
6312
7107
|
return fetch;
|
|
6313
7108
|
} catch {
|
|
6314
|
-
|
|
7109
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
7110
|
+
return fetchPolyfill;
|
|
7111
|
+
}
|
|
7112
|
+
throw new Error("AbortController is not available. This should not happen in Node.js 18+ or modern browsers.");
|
|
6315
7113
|
}
|
|
6316
7114
|
}
|
|
6317
7115
|
if (typeof XMLHttpRequest !== "undefined") {
|
|
6318
7116
|
return fetchPolyfill;
|
|
6319
7117
|
}
|
|
6320
|
-
throw new Error("fetch is not available
|
|
7118
|
+
throw new Error("fetch is not available. Node.js 18+ is required (engines.node >= 18.0.0). For browser environments, please use a modern browser or provide a fetch polyfill.");
|
|
6321
7119
|
}
|
|
6322
|
-
async function
|
|
7120
|
+
async function universalRequest(url, options = {}) {
|
|
6323
7121
|
const { method = "POST", headers = {}, body, timeout = 1e4 } = options;
|
|
6324
7122
|
const AbortControllerClass = getAbortController();
|
|
6325
7123
|
const controller = new AbortControllerClass();
|
|
@@ -6354,64 +7152,8 @@ async function browserRequest(url, options = {}) {
|
|
|
6354
7152
|
throw error;
|
|
6355
7153
|
}
|
|
6356
7154
|
}
|
|
6357
|
-
async function nodeRequest(url, options = {}) {
|
|
6358
|
-
var _a;
|
|
6359
|
-
const { method = "POST", headers = {}, body, timeout = 1e4 } = options;
|
|
6360
|
-
try {
|
|
6361
|
-
let nodeFetchFn;
|
|
6362
|
-
try {
|
|
6363
|
-
const nodeFetch = await import("node-fetch");
|
|
6364
|
-
nodeFetchFn = nodeFetch.default || nodeFetch;
|
|
6365
|
-
} catch (e) {
|
|
6366
|
-
if (typeof process !== "undefined" && ((_a = process.versions) == null ? void 0 : _a.node)) {
|
|
6367
|
-
throw new Error("node-fetch is required in Node.js environment. Please install it: pnpm add node-fetch");
|
|
6368
|
-
}
|
|
6369
|
-
throw new Error("Network request failed: node-fetch not available");
|
|
6370
|
-
}
|
|
6371
|
-
const AbortControllerClass = getAbortController();
|
|
6372
|
-
const controller = new AbortControllerClass();
|
|
6373
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
6374
|
-
try {
|
|
6375
|
-
const response = await nodeFetchFn(url, {
|
|
6376
|
-
method,
|
|
6377
|
-
headers: {
|
|
6378
|
-
"Content-Type": "application/json",
|
|
6379
|
-
...headers
|
|
6380
|
-
},
|
|
6381
|
-
body,
|
|
6382
|
-
signal: controller.signal
|
|
6383
|
-
});
|
|
6384
|
-
clearTimeout(timeoutId);
|
|
6385
|
-
let data;
|
|
6386
|
-
const contentType = response.headers.get("content-type");
|
|
6387
|
-
if (contentType && contentType.includes("application/json")) {
|
|
6388
|
-
data = await response.json();
|
|
6389
|
-
} else {
|
|
6390
|
-
data = await response.text();
|
|
6391
|
-
}
|
|
6392
|
-
return {
|
|
6393
|
-
ok: response.ok,
|
|
6394
|
-
status: response.status,
|
|
6395
|
-
statusText: response.statusText,
|
|
6396
|
-
data
|
|
6397
|
-
};
|
|
6398
|
-
} catch (error) {
|
|
6399
|
-
clearTimeout(timeoutId);
|
|
6400
|
-
throw error;
|
|
6401
|
-
}
|
|
6402
|
-
} catch (error) {
|
|
6403
|
-
throw error;
|
|
6404
|
-
}
|
|
6405
|
-
}
|
|
6406
7155
|
async function request(url, options = {}) {
|
|
6407
|
-
|
|
6408
|
-
if (typeof process !== "undefined" && ((_a = process.versions) == null ? void 0 : _a.node)) {
|
|
6409
|
-
return nodeRequest(url, options);
|
|
6410
|
-
}
|
|
6411
|
-
if (typeof window !== "undefined") {
|
|
6412
|
-
return browserRequest(url, options);
|
|
6413
|
-
}
|
|
6414
|
-
return browserRequest(url, options);
|
|
7156
|
+
return universalRequest(url, options);
|
|
6415
7157
|
}
|
|
6416
7158
|
function post(url, data, options = {}) {
|
|
6417
7159
|
const body = typeof data === "string" ? data : JSON.stringify(data);
|
|
@@ -6428,14 +7170,15 @@ class RetryHandler {
|
|
|
6428
7170
|
maxDelay = 1e4,
|
|
6429
7171
|
shouldRetry = () => true
|
|
6430
7172
|
} = options;
|
|
7173
|
+
const actualMaxRetries = Math.min(maxRetries, 100);
|
|
6431
7174
|
let lastError;
|
|
6432
7175
|
let delay = initialDelay;
|
|
6433
|
-
for (let attempt = 0; attempt <=
|
|
7176
|
+
for (let attempt = 0; attempt <= actualMaxRetries; attempt++) {
|
|
6434
7177
|
try {
|
|
6435
7178
|
return await fn();
|
|
6436
7179
|
} catch (error) {
|
|
6437
7180
|
lastError = error;
|
|
6438
|
-
if (attempt <
|
|
7181
|
+
if (attempt < actualMaxRetries && shouldRetry(error)) {
|
|
6439
7182
|
await this.sleep(delay);
|
|
6440
7183
|
delay = Math.min(delay * 2, maxDelay);
|
|
6441
7184
|
} else {
|
|
@@ -7116,8 +7859,8 @@ class BatchSender {
|
|
|
7116
7859
|
__publicField(this, "eventCallbacks", /* @__PURE__ */ new Map());
|
|
7117
7860
|
this.sender = new Sender(serverUrl);
|
|
7118
7861
|
this.offlineStorage = new OfflineStorage(options.maxOfflineStorageSize ?? 1e3);
|
|
7119
|
-
this.batchSize = options.batchSize
|
|
7120
|
-
this.batchWait = options.batchWait
|
|
7862
|
+
this.batchSize = options.batchSize ?? 10;
|
|
7863
|
+
this.batchWait = options.batchWait ?? 5e3;
|
|
7121
7864
|
this.enableOfflineStorage = options.enableOfflineStorage ?? true;
|
|
7122
7865
|
this.initPromise = this.init();
|
|
7123
7866
|
}
|
|
@@ -7147,10 +7890,18 @@ class BatchSender {
|
|
|
7147
7890
|
if (options == null ? void 0 : options.onBatchComplete) {
|
|
7148
7891
|
this.eventCallbacks.set(event, options);
|
|
7149
7892
|
}
|
|
7893
|
+
if (this.batchSize === 0) {
|
|
7894
|
+
this.flush();
|
|
7895
|
+
return;
|
|
7896
|
+
}
|
|
7150
7897
|
if (this.queue.length >= this.batchSize) {
|
|
7151
7898
|
this.flush();
|
|
7152
7899
|
return;
|
|
7153
7900
|
}
|
|
7901
|
+
if (this.batchWait === 0) {
|
|
7902
|
+
this.flush();
|
|
7903
|
+
return;
|
|
7904
|
+
}
|
|
7154
7905
|
if (!this.timer) {
|
|
7155
7906
|
this.timer = setTimeout(() => {
|
|
7156
7907
|
this.flush();
|
|
@@ -7628,192 +8379,1883 @@ class SessionReplay {
|
|
|
7628
8379
|
this.clear();
|
|
7629
8380
|
}
|
|
7630
8381
|
}
|
|
7631
|
-
class
|
|
7632
|
-
constructor(
|
|
7633
|
-
__publicField(this, "
|
|
7634
|
-
__publicField(this, "
|
|
7635
|
-
__publicField(this, "
|
|
7636
|
-
__publicField(this, "
|
|
7637
|
-
__publicField(this, "
|
|
7638
|
-
__publicField(this, "
|
|
7639
|
-
__publicField(this, "
|
|
7640
|
-
__publicField(this, "
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
this.
|
|
7652
|
-
this.
|
|
7653
|
-
this.
|
|
7654
|
-
|
|
7655
|
-
this.batchSender = new BatchSender(serverUrl, {
|
|
7656
|
-
batchSize: this.config.get("batchSize"),
|
|
7657
|
-
batchWait: this.config.get("batchWait"),
|
|
7658
|
-
enableOfflineStorage: this.config.get("enableOfflineStorage") ?? true,
|
|
7659
|
-
// 未设置serverUrl时强制启用离线存储
|
|
7660
|
-
maxOfflineStorageSize: this.config.get("maxOfflineStorageSize")
|
|
7661
|
-
});
|
|
7662
|
-
if (!config.serverUrl) {
|
|
7663
|
-
this.batchSender.setServerUrl("");
|
|
7664
|
-
}
|
|
7665
|
-
this.pluginManager = new PluginManager();
|
|
7666
|
-
this.behaviorAnalysis = new BehaviorAnalysis();
|
|
7667
|
-
this.pluginManager.setTrackerInstance({
|
|
7668
|
-
send: this.send.bind(this),
|
|
7669
|
-
getConfig: () => this.config.getAll(),
|
|
7670
|
-
setUser: this.setUser.bind(this),
|
|
7671
|
-
getUser: () => this.userInfo
|
|
7672
|
-
});
|
|
8382
|
+
class PerformanceMonitor {
|
|
8383
|
+
constructor(options = {}) {
|
|
8384
|
+
__publicField(this, "events", []);
|
|
8385
|
+
__publicField(this, "alerts", []);
|
|
8386
|
+
__publicField(this, "alertRules", []);
|
|
8387
|
+
__publicField(this, "options");
|
|
8388
|
+
__publicField(this, "storageKey");
|
|
8389
|
+
__publicField(this, "maxStoredEvents");
|
|
8390
|
+
__publicField(this, "retentionTime");
|
|
8391
|
+
__publicField(this, "cleanupTimer", null);
|
|
8392
|
+
this.options = {
|
|
8393
|
+
enableAlerts: options.enableAlerts ?? true,
|
|
8394
|
+
defaultAlertRules: options.defaultAlertRules ?? this.getDefaultAlertRules(),
|
|
8395
|
+
onAlert: options.onAlert ?? (() => {
|
|
8396
|
+
}),
|
|
8397
|
+
storageKey: options.storageKey ?? "__tracker_performance_events__",
|
|
8398
|
+
maxStoredEvents: options.maxStoredEvents ?? 1e4,
|
|
8399
|
+
retentionTime: options.retentionTime ?? 7 * 24 * 60 * 60 * 1e3
|
|
8400
|
+
// 7天
|
|
8401
|
+
};
|
|
8402
|
+
this.storageKey = this.options.storageKey;
|
|
8403
|
+
this.maxStoredEvents = this.options.maxStoredEvents;
|
|
8404
|
+
this.retentionTime = this.options.retentionTime;
|
|
8405
|
+
this.alertRules = this.options.defaultAlertRules;
|
|
7673
8406
|
this.init();
|
|
7674
|
-
if (typeof window !== "undefined") {
|
|
7675
|
-
window.__trackerEventBus = {
|
|
7676
|
-
emit: (event, data) => {
|
|
7677
|
-
eventBus.emit(event, data);
|
|
7678
|
-
}
|
|
7679
|
-
};
|
|
7680
|
-
}
|
|
7681
8407
|
}
|
|
7682
8408
|
/**
|
|
7683
8409
|
* 初始化
|
|
7684
8410
|
*/
|
|
7685
8411
|
init() {
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
this.
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
}
|
|
7695
|
-
if (this.config.get("enablePerformanceTrack")) {
|
|
7696
|
-
this.performanceTracker = new PerformanceTracker();
|
|
7697
|
-
this.performanceTracker.init();
|
|
7698
|
-
}
|
|
7699
|
-
if (this.config.get("enableSessionReplay")) {
|
|
7700
|
-
this.sessionReplay = new SessionReplay();
|
|
7701
|
-
this.sessionReplay.start();
|
|
7702
|
-
}
|
|
7703
|
-
if (this.config.get("userId")) {
|
|
7704
|
-
this.setUser({ userId: this.config.get("userId") });
|
|
7705
|
-
}
|
|
8412
|
+
const handler = (...args) => {
|
|
8413
|
+
const event = args[0];
|
|
8414
|
+
this.handlePerformanceEvent(event);
|
|
8415
|
+
};
|
|
8416
|
+
eventBus.on("performance", handler);
|
|
8417
|
+
this._performanceHandler = handler;
|
|
8418
|
+
this.loadHistoryData();
|
|
8419
|
+
this.startCleanupTimer();
|
|
7706
8420
|
}
|
|
7707
8421
|
/**
|
|
7708
|
-
*
|
|
8422
|
+
* 处理性能事件
|
|
7709
8423
|
*/
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
});
|
|
7720
|
-
eventBus.on("input", (event) => {
|
|
7721
|
-
this.send(event);
|
|
7722
|
-
});
|
|
7723
|
-
eventBus.on("error", (event) => {
|
|
7724
|
-
this.send(event);
|
|
7725
|
-
});
|
|
7726
|
-
eventBus.on("performance", (event) => {
|
|
7727
|
-
this.send(event);
|
|
7728
|
-
});
|
|
8424
|
+
handlePerformanceEvent(event) {
|
|
8425
|
+
this.events.push(event);
|
|
8426
|
+
if (this.events.length > this.maxStoredEvents) {
|
|
8427
|
+
this.events = this.events.slice(-this.maxStoredEvents);
|
|
8428
|
+
}
|
|
8429
|
+
if (this.options.enableAlerts) {
|
|
8430
|
+
this.checkAlertRules(event);
|
|
8431
|
+
}
|
|
8432
|
+
this.saveToStorage();
|
|
7729
8433
|
}
|
|
7730
8434
|
/**
|
|
7731
|
-
*
|
|
8435
|
+
* 检查告警规则
|
|
7732
8436
|
*/
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
});
|
|
7741
|
-
if (!processedEvent) {
|
|
7742
|
-
return;
|
|
8437
|
+
checkAlertRules(event) {
|
|
8438
|
+
if (event.metricType !== "navigation" && event.metricType !== "longtask") {
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
for (const rule of this.alertRules) {
|
|
8442
|
+
if (!rule.enabled) {
|
|
8443
|
+
continue;
|
|
7743
8444
|
}
|
|
7744
|
-
|
|
7745
|
-
if (
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
8445
|
+
let value;
|
|
8446
|
+
if (rule.metric === "ttfb" && event.metricType === "navigation") {
|
|
8447
|
+
value = event.metrics.ttfb;
|
|
8448
|
+
} else if (rule.metric === "total" && event.metricType === "navigation") {
|
|
8449
|
+
value = event.metrics.total;
|
|
8450
|
+
} else if (rule.metric === "longtask" && event.metricType === "longtask") {
|
|
8451
|
+
value = event.metrics.duration;
|
|
8452
|
+
} else {
|
|
8453
|
+
continue;
|
|
7753
8454
|
}
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
const afterSend = this.config.get("afterSend");
|
|
7757
|
-
if (afterSend) {
|
|
7758
|
-
events.forEach((event2) => {
|
|
7759
|
-
setTimeout(() => {
|
|
7760
|
-
afterSend(event2, success);
|
|
7761
|
-
}, 0);
|
|
7762
|
-
});
|
|
7763
|
-
}
|
|
7764
|
-
}
|
|
7765
|
-
});
|
|
7766
|
-
if (event.type === "pageview") {
|
|
7767
|
-
this.behaviorAnalysis.analyzePageView(processedEvent);
|
|
8455
|
+
if (value === void 0) {
|
|
8456
|
+
continue;
|
|
7768
8457
|
}
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
8458
|
+
let triggered = false;
|
|
8459
|
+
switch (rule.operator) {
|
|
8460
|
+
case ">":
|
|
8461
|
+
triggered = value > rule.threshold;
|
|
8462
|
+
break;
|
|
8463
|
+
case ">=":
|
|
8464
|
+
triggered = value >= rule.threshold;
|
|
8465
|
+
break;
|
|
8466
|
+
case "<":
|
|
8467
|
+
triggered = value < rule.threshold;
|
|
8468
|
+
break;
|
|
8469
|
+
case "<=":
|
|
8470
|
+
triggered = value <= rule.threshold;
|
|
8471
|
+
break;
|
|
8472
|
+
}
|
|
8473
|
+
if (triggered) {
|
|
8474
|
+
this.triggerAlert(rule, value, event);
|
|
7775
8475
|
}
|
|
7776
8476
|
}
|
|
7777
8477
|
}
|
|
7778
8478
|
/**
|
|
7779
|
-
*
|
|
8479
|
+
* 触发告警
|
|
7780
8480
|
*/
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
...customProperties,
|
|
7788
|
-
...eventProperties
|
|
7789
|
-
};
|
|
7790
|
-
const finalProperties = Object.keys(mergedProperties).length > 0 ? mergedProperties : void 0;
|
|
7791
|
-
const enriched = {
|
|
7792
|
-
...event,
|
|
7793
|
-
deviceId: event.deviceId || this.deviceId,
|
|
7794
|
-
sessionId: event.sessionId || this.sessionId
|
|
7795
|
-
};
|
|
7796
|
-
if (userId && userId !== "") {
|
|
7797
|
-
enriched.userId = userId;
|
|
8481
|
+
triggerAlert(rule, value, event) {
|
|
8482
|
+
let level = "warning";
|
|
8483
|
+
if (value > rule.threshold * 2) {
|
|
8484
|
+
level = "critical";
|
|
8485
|
+
} else if (value > rule.threshold * 1.5) {
|
|
8486
|
+
level = "error";
|
|
7798
8487
|
}
|
|
7799
|
-
|
|
7800
|
-
|
|
8488
|
+
const alert2 = {
|
|
8489
|
+
id: `alert-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
|
|
8490
|
+
ruleId: rule.id,
|
|
8491
|
+
level,
|
|
8492
|
+
message: rule.message || `${rule.name}: ${value.toFixed(2)}ms 超过阈值 ${rule.threshold}ms`,
|
|
8493
|
+
value,
|
|
8494
|
+
threshold: rule.threshold,
|
|
8495
|
+
timestamp: Date.now(),
|
|
8496
|
+
url: event.url,
|
|
8497
|
+
resolved: false
|
|
8498
|
+
};
|
|
8499
|
+
this.alerts.push(alert2);
|
|
8500
|
+
if (this.alerts.length > 1e3) {
|
|
8501
|
+
this.alerts = this.alerts.slice(-1e3);
|
|
7801
8502
|
}
|
|
7802
|
-
|
|
8503
|
+
this.options.onAlert(alert2);
|
|
7803
8504
|
}
|
|
7804
8505
|
/**
|
|
7805
|
-
*
|
|
8506
|
+
* 获取默认告警规则
|
|
7806
8507
|
*/
|
|
7807
|
-
|
|
7808
|
-
|
|
7809
|
-
|
|
8508
|
+
getDefaultAlertRules() {
|
|
8509
|
+
return [
|
|
8510
|
+
{
|
|
8511
|
+
id: "ttfb-warning",
|
|
8512
|
+
name: "TTFB告警",
|
|
8513
|
+
metric: "ttfb",
|
|
8514
|
+
threshold: 1e3,
|
|
8515
|
+
operator: ">",
|
|
8516
|
+
enabled: true,
|
|
8517
|
+
message: "TTFB超过1000ms,可能影响用户体验"
|
|
8518
|
+
},
|
|
8519
|
+
{
|
|
8520
|
+
id: "total-load-warning",
|
|
8521
|
+
name: "总加载时间告警",
|
|
8522
|
+
metric: "total",
|
|
8523
|
+
threshold: 3e3,
|
|
8524
|
+
operator: ">",
|
|
8525
|
+
enabled: true,
|
|
8526
|
+
message: "页面总加载时间超过3000ms,可能影响用户体验"
|
|
8527
|
+
},
|
|
8528
|
+
{
|
|
8529
|
+
id: "longtask-warning",
|
|
8530
|
+
name: "长任务告警",
|
|
8531
|
+
metric: "longtask",
|
|
8532
|
+
threshold: 50,
|
|
8533
|
+
operator: ">",
|
|
8534
|
+
enabled: true,
|
|
8535
|
+
message: "检测到长任务超过50ms,可能阻塞主线程"
|
|
8536
|
+
}
|
|
8537
|
+
];
|
|
7810
8538
|
}
|
|
7811
8539
|
/**
|
|
7812
|
-
*
|
|
8540
|
+
* 获取性能指标
|
|
7813
8541
|
*/
|
|
7814
|
-
|
|
7815
|
-
|
|
7816
|
-
|
|
8542
|
+
getMetrics(startTime, endTime) {
|
|
8543
|
+
const now = Date.now();
|
|
8544
|
+
const start = startTime || now - 36e5;
|
|
8545
|
+
const end = endTime || now;
|
|
8546
|
+
const filteredEvents = this.events.filter(
|
|
8547
|
+
(e) => e.timestamp >= start && e.timestamp <= end
|
|
8548
|
+
);
|
|
8549
|
+
const navigationEvents = filteredEvents.filter(
|
|
8550
|
+
(e) => e.metricType === "navigation"
|
|
8551
|
+
);
|
|
8552
|
+
const resourceEvents = filteredEvents.filter(
|
|
8553
|
+
(e) => e.metricType === "resource"
|
|
8554
|
+
);
|
|
8555
|
+
const longTaskEvents = filteredEvents.filter(
|
|
8556
|
+
(e) => e.metricType === "longtask"
|
|
8557
|
+
);
|
|
8558
|
+
let navigation = {
|
|
8559
|
+
dns: 0,
|
|
8560
|
+
tcp: 0,
|
|
8561
|
+
ssl: 0,
|
|
8562
|
+
ttfb: 0,
|
|
8563
|
+
response: 0,
|
|
8564
|
+
domParse: 0,
|
|
8565
|
+
resourceLoad: 0,
|
|
8566
|
+
total: 0,
|
|
8567
|
+
domContentLoaded: 0,
|
|
8568
|
+
load: 0
|
|
8569
|
+
};
|
|
8570
|
+
if (navigationEvents.length > 0) {
|
|
8571
|
+
const metrics = navigationEvents.map((e) => e.metrics);
|
|
8572
|
+
navigation = {
|
|
8573
|
+
dns: this.average(metrics, "dns"),
|
|
8574
|
+
tcp: this.average(metrics, "tcp"),
|
|
8575
|
+
ssl: this.average(metrics, "ssl"),
|
|
8576
|
+
ttfb: this.average(metrics, "ttfb"),
|
|
8577
|
+
response: this.average(metrics, "response"),
|
|
8578
|
+
domParse: this.average(metrics, "domParse"),
|
|
8579
|
+
resourceLoad: this.average(metrics, "resourceLoad"),
|
|
8580
|
+
total: this.average(metrics, "total"),
|
|
8581
|
+
domContentLoaded: this.average(metrics, "domContentLoaded"),
|
|
8582
|
+
load: this.average(metrics, "load")
|
|
8583
|
+
};
|
|
8584
|
+
}
|
|
8585
|
+
const resources = {
|
|
8586
|
+
total: resourceEvents.length,
|
|
8587
|
+
avgLoadTime: 0,
|
|
8588
|
+
maxLoadTime: 0,
|
|
8589
|
+
totalSize: 0,
|
|
8590
|
+
byType: {}
|
|
8591
|
+
};
|
|
8592
|
+
if (resourceEvents.length > 0) {
|
|
8593
|
+
const loadTimes = resourceEvents.map((e) => e.metrics.total || 0).filter((t) => t > 0);
|
|
8594
|
+
if (loadTimes.length > 0) {
|
|
8595
|
+
resources.avgLoadTime = loadTimes.reduce((a, b) => a + b, 0) / loadTimes.length;
|
|
8596
|
+
resources.maxLoadTime = Math.max(...loadTimes);
|
|
8597
|
+
}
|
|
8598
|
+
resources.totalSize = resourceEvents.reduce(
|
|
8599
|
+
(sum, e) => sum + (e.metrics.size || 0),
|
|
8600
|
+
0
|
|
8601
|
+
);
|
|
8602
|
+
const typeMap = /* @__PURE__ */ new Map();
|
|
8603
|
+
resourceEvents.forEach((e) => {
|
|
8604
|
+
const metricsWithString = e.metrics;
|
|
8605
|
+
const type = metricsWithString.resourceType || "unknown";
|
|
8606
|
+
if (!typeMap.has(type)) {
|
|
8607
|
+
typeMap.set(type, []);
|
|
8608
|
+
}
|
|
8609
|
+
typeMap.get(type).push(metricsWithString.total || 0);
|
|
8610
|
+
});
|
|
8611
|
+
typeMap.forEach((times, type) => {
|
|
8612
|
+
const typeEvents = resourceEvents.filter((e) => {
|
|
8613
|
+
const metricsWithString = e.metrics;
|
|
8614
|
+
return metricsWithString.resourceType === type;
|
|
8615
|
+
});
|
|
8616
|
+
resources.byType[type] = {
|
|
8617
|
+
count: times.length,
|
|
8618
|
+
avgLoadTime: times.reduce((a, b) => a + b, 0) / times.length,
|
|
8619
|
+
totalSize: typeEvents.reduce((sum, e) => {
|
|
8620
|
+
const metricsWithString = e.metrics;
|
|
8621
|
+
return sum + (metricsWithString.size || 0);
|
|
8622
|
+
}, 0)
|
|
8623
|
+
};
|
|
8624
|
+
});
|
|
8625
|
+
}
|
|
8626
|
+
const longTasks = {
|
|
8627
|
+
total: longTaskEvents.length,
|
|
8628
|
+
avgDuration: 0,
|
|
8629
|
+
maxDuration: 0,
|
|
8630
|
+
recent: longTaskEvents.slice(-10).map((e) => ({
|
|
8631
|
+
duration: e.metrics.duration || 0,
|
|
8632
|
+
startTime: e.metrics.startTime || 0,
|
|
8633
|
+
timestamp: e.timestamp
|
|
8634
|
+
}))
|
|
8635
|
+
};
|
|
8636
|
+
if (longTaskEvents.length > 0) {
|
|
8637
|
+
const durations = longTaskEvents.map((e) => e.metrics.duration || 0).filter((d) => d > 0);
|
|
8638
|
+
if (durations.length > 0) {
|
|
8639
|
+
longTasks.avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
|
|
8640
|
+
longTasks.maxDuration = Math.max(...durations);
|
|
8641
|
+
}
|
|
8642
|
+
}
|
|
8643
|
+
return {
|
|
8644
|
+
navigation,
|
|
8645
|
+
resources,
|
|
8646
|
+
longTasks,
|
|
8647
|
+
timeRange: {
|
|
8648
|
+
start,
|
|
8649
|
+
end
|
|
8650
|
+
}
|
|
8651
|
+
};
|
|
8652
|
+
}
|
|
8653
|
+
/**
|
|
8654
|
+
* 计算平均值
|
|
8655
|
+
*/
|
|
8656
|
+
average(metrics, key) {
|
|
8657
|
+
const values = metrics.map((m) => m[key] || 0).filter((v) => v > 0);
|
|
8658
|
+
if (values.length === 0) {
|
|
8659
|
+
return 0;
|
|
8660
|
+
}
|
|
8661
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
8662
|
+
}
|
|
8663
|
+
/**
|
|
8664
|
+
* 生成性能报告
|
|
8665
|
+
*/
|
|
8666
|
+
generateReport(options = {}) {
|
|
8667
|
+
const now = Date.now();
|
|
8668
|
+
const startTime = options.startTime || (options.duration ? now - options.duration : now - 36e5);
|
|
8669
|
+
const endTime = options.endTime || now;
|
|
8670
|
+
const metrics = this.getMetrics(startTime, endTime);
|
|
8671
|
+
const filteredAlerts = this.alerts.filter(
|
|
8672
|
+
(a) => a.timestamp >= startTime && a.timestamp <= endTime
|
|
8673
|
+
);
|
|
8674
|
+
const navigationEvents = this.events.filter(
|
|
8675
|
+
(e) => e.metricType === "navigation" && e.timestamp >= startTime && e.timestamp <= endTime
|
|
8676
|
+
);
|
|
8677
|
+
const summary = {
|
|
8678
|
+
pageLoads: navigationEvents.length,
|
|
8679
|
+
avgTTFB: metrics.navigation.ttfb,
|
|
8680
|
+
avgTotalLoadTime: metrics.navigation.total,
|
|
8681
|
+
totalLongTasks: metrics.longTasks.total,
|
|
8682
|
+
totalResources: metrics.resources.total
|
|
8683
|
+
};
|
|
8684
|
+
const trends = this.generateTrends(startTime, endTime);
|
|
8685
|
+
return {
|
|
8686
|
+
id: `report-${Date.now()}`,
|
|
8687
|
+
generatedAt: now,
|
|
8688
|
+
timeRange: {
|
|
8689
|
+
start: startTime,
|
|
8690
|
+
end: endTime
|
|
8691
|
+
},
|
|
8692
|
+
summary,
|
|
8693
|
+
metrics,
|
|
8694
|
+
alerts: filteredAlerts,
|
|
8695
|
+
trends
|
|
8696
|
+
};
|
|
8697
|
+
}
|
|
8698
|
+
/**
|
|
8699
|
+
* 生成趋势数据
|
|
8700
|
+
*/
|
|
8701
|
+
generateTrends(startTime, endTime) {
|
|
8702
|
+
const trends = [];
|
|
8703
|
+
const interval = 36e5;
|
|
8704
|
+
let current = startTime;
|
|
8705
|
+
while (current < endTime) {
|
|
8706
|
+
const next = Math.min(current + interval, endTime);
|
|
8707
|
+
const hourMetrics = this.getMetrics(current, next);
|
|
8708
|
+
trends.push({
|
|
8709
|
+
time: current,
|
|
8710
|
+
metrics: {
|
|
8711
|
+
navigation: hourMetrics.navigation,
|
|
8712
|
+
resources: {
|
|
8713
|
+
total: hourMetrics.resources.total,
|
|
8714
|
+
avgLoadTime: hourMetrics.resources.avgLoadTime,
|
|
8715
|
+
maxLoadTime: hourMetrics.resources.maxLoadTime,
|
|
8716
|
+
totalSize: hourMetrics.resources.totalSize,
|
|
8717
|
+
byType: hourMetrics.resources.byType
|
|
8718
|
+
},
|
|
8719
|
+
longTasks: {
|
|
8720
|
+
total: hourMetrics.longTasks.total,
|
|
8721
|
+
avgDuration: hourMetrics.longTasks.avgDuration,
|
|
8722
|
+
maxDuration: hourMetrics.longTasks.maxDuration,
|
|
8723
|
+
recent: hourMetrics.longTasks.recent
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
});
|
|
8727
|
+
current = next;
|
|
8728
|
+
}
|
|
8729
|
+
return trends;
|
|
8730
|
+
}
|
|
8731
|
+
/**
|
|
8732
|
+
* 获取告警列表
|
|
8733
|
+
*/
|
|
8734
|
+
getAlerts(resolved) {
|
|
8735
|
+
if (resolved === void 0) {
|
|
8736
|
+
return [...this.alerts];
|
|
8737
|
+
}
|
|
8738
|
+
return this.alerts.filter((a) => a.resolved === resolved);
|
|
8739
|
+
}
|
|
8740
|
+
/**
|
|
8741
|
+
* 添加告警规则
|
|
8742
|
+
*/
|
|
8743
|
+
addAlertRule(rule) {
|
|
8744
|
+
this.alertRules.push(rule);
|
|
8745
|
+
}
|
|
8746
|
+
/**
|
|
8747
|
+
* 移除告警规则
|
|
8748
|
+
*/
|
|
8749
|
+
removeAlertRule(ruleId) {
|
|
8750
|
+
const index = this.alertRules.findIndex((r) => r.id === ruleId);
|
|
8751
|
+
if (index >= 0) {
|
|
8752
|
+
this.alertRules.splice(index, 1);
|
|
8753
|
+
return true;
|
|
8754
|
+
}
|
|
8755
|
+
return false;
|
|
8756
|
+
}
|
|
8757
|
+
/**
|
|
8758
|
+
* 获取告警规则
|
|
8759
|
+
*/
|
|
8760
|
+
getAlertRules() {
|
|
8761
|
+
return [...this.alertRules];
|
|
8762
|
+
}
|
|
8763
|
+
/**
|
|
8764
|
+
* 标记告警为已处理
|
|
8765
|
+
*/
|
|
8766
|
+
resolveAlert(alertId) {
|
|
8767
|
+
const alert2 = this.alerts.find((a) => a.id === alertId);
|
|
8768
|
+
if (alert2) {
|
|
8769
|
+
alert2.resolved = true;
|
|
8770
|
+
return true;
|
|
8771
|
+
}
|
|
8772
|
+
return false;
|
|
8773
|
+
}
|
|
8774
|
+
/**
|
|
8775
|
+
* 保存到存储
|
|
8776
|
+
*/
|
|
8777
|
+
async saveToStorage() {
|
|
8778
|
+
try {
|
|
8779
|
+
const data = {
|
|
8780
|
+
events: this.events.slice(-1e3),
|
|
8781
|
+
// 只保存最近1000个事件
|
|
8782
|
+
alerts: this.alerts.slice(-100),
|
|
8783
|
+
// 只保存最近100个告警
|
|
8784
|
+
timestamp: Date.now()
|
|
8785
|
+
};
|
|
8786
|
+
await indexedDBStorage.setItem(this.storageKey, JSON.stringify(data));
|
|
8787
|
+
} catch (error) {
|
|
8788
|
+
console.error("PerformanceMonitor: Failed to save to storage:", error);
|
|
8789
|
+
}
|
|
8790
|
+
}
|
|
8791
|
+
/**
|
|
8792
|
+
* 加载历史数据
|
|
8793
|
+
*/
|
|
8794
|
+
async loadHistoryData() {
|
|
8795
|
+
try {
|
|
8796
|
+
const dataStr = await indexedDBStorage.getItem(this.storageKey);
|
|
8797
|
+
if (dataStr) {
|
|
8798
|
+
const data = JSON.parse(dataStr);
|
|
8799
|
+
if (data.events) {
|
|
8800
|
+
this.events = data.events;
|
|
8801
|
+
}
|
|
8802
|
+
if (data.alerts) {
|
|
8803
|
+
this.alerts = data.alerts;
|
|
8804
|
+
}
|
|
8805
|
+
}
|
|
8806
|
+
} catch (error) {
|
|
8807
|
+
console.error("PerformanceMonitor: Failed to load history data:", error);
|
|
8808
|
+
}
|
|
8809
|
+
}
|
|
8810
|
+
/**
|
|
8811
|
+
* 启动清理定时器
|
|
8812
|
+
*/
|
|
8813
|
+
startCleanupTimer() {
|
|
8814
|
+
if (this.cleanupTimer || typeof window === "undefined") {
|
|
8815
|
+
return;
|
|
8816
|
+
}
|
|
8817
|
+
this.cleanupTimer = window.setInterval(() => {
|
|
8818
|
+
this.cleanup();
|
|
8819
|
+
}, 36e5);
|
|
8820
|
+
}
|
|
8821
|
+
/**
|
|
8822
|
+
* 清理过期数据
|
|
8823
|
+
*/
|
|
8824
|
+
cleanup() {
|
|
8825
|
+
const now = Date.now();
|
|
8826
|
+
const cutoffTime = now - this.retentionTime;
|
|
8827
|
+
this.events = this.events.filter((e) => e.timestamp >= cutoffTime);
|
|
8828
|
+
this.alerts = this.alerts.filter((a) => a.timestamp >= cutoffTime);
|
|
8829
|
+
this.saveToStorage();
|
|
8830
|
+
}
|
|
8831
|
+
/**
|
|
8832
|
+
* 销毁
|
|
8833
|
+
*/
|
|
8834
|
+
destroy() {
|
|
8835
|
+
const handler = this._performanceHandler;
|
|
8836
|
+
if (handler) {
|
|
8837
|
+
eventBus.off("performance", handler);
|
|
8838
|
+
}
|
|
8839
|
+
if (this.cleanupTimer && typeof window !== "undefined") {
|
|
8840
|
+
clearInterval(this.cleanupTimer);
|
|
8841
|
+
this.cleanupTimer = null;
|
|
8842
|
+
}
|
|
8843
|
+
}
|
|
8844
|
+
}
|
|
8845
|
+
const PERFORMANCE_PANEL_HTML = `
|
|
8846
|
+
<!DOCTYPE html>
|
|
8847
|
+
<html lang="zh-CN">
|
|
8848
|
+
<head>
|
|
8849
|
+
<meta charset="UTF-8">
|
|
8850
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8851
|
+
<title>性能监控面板 - Tracker SDK</title>
|
|
8852
|
+
<style>
|
|
8853
|
+
* {
|
|
8854
|
+
margin: 0;
|
|
8855
|
+
padding: 0;
|
|
8856
|
+
box-sizing: border-box;
|
|
8857
|
+
}
|
|
8858
|
+
body {
|
|
8859
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
8860
|
+
background: #f5f5f5;
|
|
8861
|
+
padding: 20px;
|
|
8862
|
+
}
|
|
8863
|
+
.container {
|
|
8864
|
+
max-width: 1400px;
|
|
8865
|
+
margin: 0 auto;
|
|
8866
|
+
background: white;
|
|
8867
|
+
border-radius: 8px;
|
|
8868
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
8869
|
+
padding: 30px;
|
|
8870
|
+
}
|
|
8871
|
+
h1 {
|
|
8872
|
+
color: #333;
|
|
8873
|
+
margin-bottom: 10px;
|
|
8874
|
+
border-bottom: 3px solid #9C27B0;
|
|
8875
|
+
padding-bottom: 10px;
|
|
8876
|
+
}
|
|
8877
|
+
.subtitle {
|
|
8878
|
+
color: #666;
|
|
8879
|
+
margin-bottom: 30px;
|
|
8880
|
+
}
|
|
8881
|
+
.toolbar {
|
|
8882
|
+
display: flex;
|
|
8883
|
+
gap: 10px;
|
|
8884
|
+
margin-bottom: 20px;
|
|
8885
|
+
flex-wrap: wrap;
|
|
8886
|
+
}
|
|
8887
|
+
button {
|
|
8888
|
+
background: #9C27B0;
|
|
8889
|
+
color: white;
|
|
8890
|
+
border: none;
|
|
8891
|
+
padding: 10px 20px;
|
|
8892
|
+
border-radius: 4px;
|
|
8893
|
+
cursor: pointer;
|
|
8894
|
+
font-size: 14px;
|
|
8895
|
+
transition: background 0.3s;
|
|
8896
|
+
}
|
|
8897
|
+
button:hover {
|
|
8898
|
+
background: #7B1FA2;
|
|
8899
|
+
}
|
|
8900
|
+
button.secondary {
|
|
8901
|
+
background: #2196F3;
|
|
8902
|
+
}
|
|
8903
|
+
button.secondary:hover {
|
|
8904
|
+
background: #1976D2;
|
|
8905
|
+
}
|
|
8906
|
+
button.success {
|
|
8907
|
+
background: #4CAF50;
|
|
8908
|
+
}
|
|
8909
|
+
button.success:hover {
|
|
8910
|
+
background: #45a049;
|
|
8911
|
+
}
|
|
8912
|
+
.stats-grid {
|
|
8913
|
+
display: grid;
|
|
8914
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
8915
|
+
gap: 15px;
|
|
8916
|
+
margin-bottom: 30px;
|
|
8917
|
+
}
|
|
8918
|
+
.stat-card {
|
|
8919
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
8920
|
+
color: white;
|
|
8921
|
+
padding: 20px;
|
|
8922
|
+
border-radius: 8px;
|
|
8923
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
8924
|
+
}
|
|
8925
|
+
.stat-card.warning {
|
|
8926
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
8927
|
+
}
|
|
8928
|
+
.stat-card.success {
|
|
8929
|
+
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
|
8930
|
+
}
|
|
8931
|
+
.stat-card.info {
|
|
8932
|
+
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
8933
|
+
}
|
|
8934
|
+
.stat-label {
|
|
8935
|
+
font-size: 12px;
|
|
8936
|
+
opacity: 0.9;
|
|
8937
|
+
margin-bottom: 5px;
|
|
8938
|
+
}
|
|
8939
|
+
.stat-value {
|
|
8940
|
+
font-size: 32px;
|
|
8941
|
+
font-weight: bold;
|
|
8942
|
+
}
|
|
8943
|
+
.stat-unit {
|
|
8944
|
+
font-size: 14px;
|
|
8945
|
+
opacity: 0.8;
|
|
8946
|
+
margin-left: 5px;
|
|
8947
|
+
}
|
|
8948
|
+
.tabs {
|
|
8949
|
+
display: flex;
|
|
8950
|
+
gap: 10px;
|
|
8951
|
+
margin-bottom: 20px;
|
|
8952
|
+
border-bottom: 2px solid #eee;
|
|
8953
|
+
}
|
|
8954
|
+
.tab {
|
|
8955
|
+
padding: 10px 20px;
|
|
8956
|
+
cursor: pointer;
|
|
8957
|
+
border: none;
|
|
8958
|
+
background: none;
|
|
8959
|
+
color: #666;
|
|
8960
|
+
font-size: 14px;
|
|
8961
|
+
border-bottom: 2px solid transparent;
|
|
8962
|
+
margin-bottom: -2px;
|
|
8963
|
+
}
|
|
8964
|
+
.tab.active {
|
|
8965
|
+
color: #9C27B0;
|
|
8966
|
+
border-bottom-color: #9C27B0;
|
|
8967
|
+
font-weight: bold;
|
|
8968
|
+
}
|
|
8969
|
+
.tab-content {
|
|
8970
|
+
display: none;
|
|
8971
|
+
}
|
|
8972
|
+
.tab-content.active {
|
|
8973
|
+
display: block;
|
|
8974
|
+
}
|
|
8975
|
+
.metrics-table {
|
|
8976
|
+
width: 100%;
|
|
8977
|
+
border-collapse: collapse;
|
|
8978
|
+
margin-top: 20px;
|
|
8979
|
+
}
|
|
8980
|
+
.metrics-table th,
|
|
8981
|
+
.metrics-table td {
|
|
8982
|
+
padding: 12px;
|
|
8983
|
+
text-align: left;
|
|
8984
|
+
border-bottom: 1px solid #eee;
|
|
8985
|
+
}
|
|
8986
|
+
.metrics-table th {
|
|
8987
|
+
background: #f9f9f9;
|
|
8988
|
+
font-weight: bold;
|
|
8989
|
+
color: #333;
|
|
8990
|
+
}
|
|
8991
|
+
.metrics-table tr:hover {
|
|
8992
|
+
background: #f9f9f9;
|
|
8993
|
+
}
|
|
8994
|
+
.alert-item {
|
|
8995
|
+
padding: 15px;
|
|
8996
|
+
border-left: 4px solid #f44336;
|
|
8997
|
+
background: #fff3f3;
|
|
8998
|
+
margin-bottom: 10px;
|
|
8999
|
+
border-radius: 4px;
|
|
9000
|
+
}
|
|
9001
|
+
.alert-item.warning {
|
|
9002
|
+
border-left-color: #FF9800;
|
|
9003
|
+
background: #fff8f0;
|
|
9004
|
+
}
|
|
9005
|
+
.alert-item.error {
|
|
9006
|
+
border-left-color: #f44336;
|
|
9007
|
+
background: #fff3f3;
|
|
9008
|
+
}
|
|
9009
|
+
.alert-item.critical {
|
|
9010
|
+
border-left-color: #d32f2f;
|
|
9011
|
+
background: #ffebee;
|
|
9012
|
+
}
|
|
9013
|
+
.alert-header {
|
|
9014
|
+
display: flex;
|
|
9015
|
+
justify-content: space-between;
|
|
9016
|
+
align-items: center;
|
|
9017
|
+
margin-bottom: 5px;
|
|
9018
|
+
}
|
|
9019
|
+
.alert-level {
|
|
9020
|
+
display: inline-block;
|
|
9021
|
+
padding: 4px 8px;
|
|
9022
|
+
border-radius: 4px;
|
|
9023
|
+
font-size: 12px;
|
|
9024
|
+
font-weight: bold;
|
|
9025
|
+
color: white;
|
|
9026
|
+
}
|
|
9027
|
+
.alert-level.warning {
|
|
9028
|
+
background: #FF9800;
|
|
9029
|
+
}
|
|
9030
|
+
.alert-level.error {
|
|
9031
|
+
background: #f44336;
|
|
9032
|
+
}
|
|
9033
|
+
.alert-level.critical {
|
|
9034
|
+
background: #d32f2f;
|
|
9035
|
+
}
|
|
9036
|
+
.alert-time {
|
|
9037
|
+
color: #999;
|
|
9038
|
+
font-size: 12px;
|
|
9039
|
+
}
|
|
9040
|
+
.chart-container {
|
|
9041
|
+
width: 100%;
|
|
9042
|
+
height: 300px;
|
|
9043
|
+
margin: 20px 0;
|
|
9044
|
+
position: relative;
|
|
9045
|
+
}
|
|
9046
|
+
.chart-bar {
|
|
9047
|
+
display: flex;
|
|
9048
|
+
align-items: flex-end;
|
|
9049
|
+
height: 250px;
|
|
9050
|
+
gap: 10px;
|
|
9051
|
+
padding: 20px;
|
|
9052
|
+
background: #f9f9f9;
|
|
9053
|
+
border-radius: 4px;
|
|
9054
|
+
}
|
|
9055
|
+
.chart-bar-item {
|
|
9056
|
+
flex: 1;
|
|
9057
|
+
background: linear-gradient(to top, #9C27B0, #7B1FA2);
|
|
9058
|
+
border-radius: 4px 4px 0 0;
|
|
9059
|
+
position: relative;
|
|
9060
|
+
min-height: 10px;
|
|
9061
|
+
}
|
|
9062
|
+
.chart-bar-label {
|
|
9063
|
+
text-align: center;
|
|
9064
|
+
margin-top: 5px;
|
|
9065
|
+
font-size: 12px;
|
|
9066
|
+
color: #666;
|
|
9067
|
+
}
|
|
9068
|
+
.chart-bar-value {
|
|
9069
|
+
position: absolute;
|
|
9070
|
+
top: -20px;
|
|
9071
|
+
left: 50%;
|
|
9072
|
+
transform: translateX(-50%);
|
|
9073
|
+
font-size: 11px;
|
|
9074
|
+
color: #333;
|
|
9075
|
+
font-weight: bold;
|
|
9076
|
+
}
|
|
9077
|
+
.loading {
|
|
9078
|
+
text-align: center;
|
|
9079
|
+
padding: 40px;
|
|
9080
|
+
color: #666;
|
|
9081
|
+
}
|
|
9082
|
+
.spinner {
|
|
9083
|
+
border: 3px solid #f3f3f3;
|
|
9084
|
+
border-top: 3px solid #9C27B0;
|
|
9085
|
+
border-radius: 50%;
|
|
9086
|
+
width: 40px;
|
|
9087
|
+
height: 40px;
|
|
9088
|
+
animation: spin 1s linear infinite;
|
|
9089
|
+
margin: 0 auto 20px;
|
|
9090
|
+
}
|
|
9091
|
+
@keyframes spin {
|
|
9092
|
+
0% { transform: rotate(0deg); }
|
|
9093
|
+
100% { transform: rotate(360deg); }
|
|
9094
|
+
}
|
|
9095
|
+
.empty-state {
|
|
9096
|
+
text-align: center;
|
|
9097
|
+
padding: 60px 20px;
|
|
9098
|
+
color: #999;
|
|
9099
|
+
}
|
|
9100
|
+
</style>
|
|
9101
|
+
</head>
|
|
9102
|
+
<body>
|
|
9103
|
+
<div class="container">
|
|
9104
|
+
<h1>📊 性能监控面板</h1>
|
|
9105
|
+
<p class="subtitle">实时性能指标监控和告警</p>
|
|
9106
|
+
|
|
9107
|
+
<div class="toolbar">
|
|
9108
|
+
<button onclick="refreshData()">🔄 刷新数据</button>
|
|
9109
|
+
<button class="secondary" onclick="generateReport()">📄 生成报告</button>
|
|
9110
|
+
<button class="success" onclick="exportData()">📥 导出数据</button>
|
|
9111
|
+
</div>
|
|
9112
|
+
|
|
9113
|
+
<div class="stats-grid" id="stats-grid">
|
|
9114
|
+
<div class="stat-card">
|
|
9115
|
+
<div class="stat-label">TTFB</div>
|
|
9116
|
+
<div class="stat-value" id="stat-ttfb">-<span class="stat-unit">ms</span></div>
|
|
9117
|
+
</div>
|
|
9118
|
+
<div class="stat-card">
|
|
9119
|
+
<div class="stat-label">总加载时间</div>
|
|
9120
|
+
<div class="stat-value" id="stat-total">-<span class="stat-unit">ms</span></div>
|
|
9121
|
+
</div>
|
|
9122
|
+
<div class="stat-card info">
|
|
9123
|
+
<div class="stat-label">资源总数</div>
|
|
9124
|
+
<div class="stat-value" id="stat-resources">-</div>
|
|
9125
|
+
</div>
|
|
9126
|
+
<div class="stat-card warning">
|
|
9127
|
+
<div class="stat-label">长任务</div>
|
|
9128
|
+
<div class="stat-value" id="stat-longtasks">-</div>
|
|
9129
|
+
</div>
|
|
9130
|
+
<div class="stat-card warning">
|
|
9131
|
+
<div class="stat-label">活跃告警</div>
|
|
9132
|
+
<div class="stat-value" id="stat-alerts">-</div>
|
|
9133
|
+
</div>
|
|
9134
|
+
</div>
|
|
9135
|
+
|
|
9136
|
+
<div class="tabs">
|
|
9137
|
+
<button class="tab active" onclick="switchTab('navigation')" data-tab="navigation">页面加载</button>
|
|
9138
|
+
<button class="tab" onclick="switchTab('resources')" data-tab="resources">资源加载</button>
|
|
9139
|
+
<button class="tab" onclick="switchTab('longtasks')" data-tab="longtasks">长任务</button>
|
|
9140
|
+
<button class="tab" onclick="switchTab('alerts')" data-tab="alerts">告警</button>
|
|
9141
|
+
</div>
|
|
9142
|
+
|
|
9143
|
+
<div id="tab-navigation" class="tab-content active">
|
|
9144
|
+
<h2>页面加载性能</h2>
|
|
9145
|
+
<div class="chart-container">
|
|
9146
|
+
<div class="chart-bar" id="navigation-chart"></div>
|
|
9147
|
+
</div>
|
|
9148
|
+
<table class="metrics-table">
|
|
9149
|
+
<thead>
|
|
9150
|
+
<tr>
|
|
9151
|
+
<th>指标</th>
|
|
9152
|
+
<th>值</th>
|
|
9153
|
+
<th>说明</th>
|
|
9154
|
+
</tr>
|
|
9155
|
+
</thead>
|
|
9156
|
+
<tbody id="navigation-metrics">
|
|
9157
|
+
</tbody>
|
|
9158
|
+
</table>
|
|
9159
|
+
</div>
|
|
9160
|
+
|
|
9161
|
+
<div id="tab-resources" class="tab-content">
|
|
9162
|
+
<h2>资源加载性能</h2>
|
|
9163
|
+
<table class="metrics-table">
|
|
9164
|
+
<thead>
|
|
9165
|
+
<tr>
|
|
9166
|
+
<th>资源类型</th>
|
|
9167
|
+
<th>数量</th>
|
|
9168
|
+
<th>平均加载时间</th>
|
|
9169
|
+
<th>总大小</th>
|
|
9170
|
+
</tr>
|
|
9171
|
+
</thead>
|
|
9172
|
+
<tbody id="resources-metrics">
|
|
9173
|
+
</tbody>
|
|
9174
|
+
</table>
|
|
9175
|
+
</div>
|
|
9176
|
+
|
|
9177
|
+
<div id="tab-longtasks" class="tab-content">
|
|
9178
|
+
<h2>长任务监控</h2>
|
|
9179
|
+
<table class="metrics-table">
|
|
9180
|
+
<thead>
|
|
9181
|
+
<tr>
|
|
9182
|
+
<th>时间</th>
|
|
9183
|
+
<th>持续时间</th>
|
|
9184
|
+
<th>开始时间</th>
|
|
9185
|
+
</tr>
|
|
9186
|
+
</thead>
|
|
9187
|
+
<tbody id="longtasks-metrics">
|
|
9188
|
+
</tbody>
|
|
9189
|
+
</table>
|
|
9190
|
+
</div>
|
|
9191
|
+
|
|
9192
|
+
<div id="tab-alerts" class="tab-content">
|
|
9193
|
+
<h2>性能告警</h2>
|
|
9194
|
+
<div id="alerts-list"></div>
|
|
9195
|
+
</div>
|
|
9196
|
+
</div>
|
|
9197
|
+
|
|
9198
|
+
<script>
|
|
9199
|
+
// 标记这是SDK内部面板,不应被监控
|
|
9200
|
+
window.__trackerInternalPanel = true;
|
|
9201
|
+
|
|
9202
|
+
let metricsData = null;
|
|
9203
|
+
let alertsData = [];
|
|
9204
|
+
|
|
9205
|
+
// 从父窗口获取Tracker实例
|
|
9206
|
+
function getTrackerInstance() {
|
|
9207
|
+
if (window.opener && window.opener.__trackerInstance) {
|
|
9208
|
+
return window.opener.__trackerInstance;
|
|
9209
|
+
}
|
|
9210
|
+
// 尝试从全局变量获取
|
|
9211
|
+
if (window.__trackerInstance) {
|
|
9212
|
+
return window.__trackerInstance;
|
|
9213
|
+
}
|
|
9214
|
+
return null;
|
|
9215
|
+
}
|
|
9216
|
+
|
|
9217
|
+
// 刷新数据
|
|
9218
|
+
async function refreshData() {
|
|
9219
|
+
const tracker = getTrackerInstance();
|
|
9220
|
+
if (!tracker) {
|
|
9221
|
+
alert('无法获取Tracker实例,请确保从Tracker打开此面板');
|
|
9222
|
+
return;
|
|
9223
|
+
}
|
|
9224
|
+
|
|
9225
|
+
try {
|
|
9226
|
+
// 获取性能指标
|
|
9227
|
+
metricsData = tracker.getPerformanceMetrics();
|
|
9228
|
+
|
|
9229
|
+
// 获取告警
|
|
9230
|
+
alertsData = tracker.getPerformanceMonitor().getAlerts(false);
|
|
9231
|
+
|
|
9232
|
+
// 更新UI
|
|
9233
|
+
updateStats();
|
|
9234
|
+
updateNavigationTab();
|
|
9235
|
+
updateResourcesTab();
|
|
9236
|
+
updateLongTasksTab();
|
|
9237
|
+
updateAlertsTab();
|
|
9238
|
+
} catch (error) {
|
|
9239
|
+
console.error('刷新数据失败:', error);
|
|
9240
|
+
alert('刷新数据失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
|
|
9244
|
+
// 更新统计卡片
|
|
9245
|
+
function updateStats() {
|
|
9246
|
+
if (!metricsData) return;
|
|
9247
|
+
|
|
9248
|
+
document.getElementById('stat-ttfb').innerHTML =
|
|
9249
|
+
Math.round(metricsData.navigation.ttfb) + '<span class="stat-unit">ms</span>';
|
|
9250
|
+
document.getElementById('stat-total').innerHTML =
|
|
9251
|
+
Math.round(metricsData.navigation.total) + '<span class="stat-unit">ms</span>';
|
|
9252
|
+
document.getElementById('stat-resources').textContent = metricsData.resources.total;
|
|
9253
|
+
document.getElementById('stat-longtasks').textContent = metricsData.longTasks.total;
|
|
9254
|
+
document.getElementById('stat-alerts').textContent = alertsData.length;
|
|
9255
|
+
}
|
|
9256
|
+
|
|
9257
|
+
// 更新页面加载标签页
|
|
9258
|
+
function updateNavigationTab() {
|
|
9259
|
+
if (!metricsData) return;
|
|
9260
|
+
|
|
9261
|
+
const nav = metricsData.navigation;
|
|
9262
|
+
const metrics = [
|
|
9263
|
+
{ name: 'DNS查询', value: nav.dns, desc: '域名解析时间' },
|
|
9264
|
+
{ name: 'TCP连接', value: nav.tcp, desc: 'TCP连接建立时间' },
|
|
9265
|
+
{ name: 'SSL握手', value: nav.ssl, desc: 'SSL/TLS握手时间' },
|
|
9266
|
+
{ name: 'TTFB', value: nav.ttfb, desc: '首字节时间' },
|
|
9267
|
+
{ name: '响应时间', value: nav.response, desc: '服务器响应时间' },
|
|
9268
|
+
{ name: 'DOM解析', value: nav.domParse, desc: 'DOM解析时间' },
|
|
9269
|
+
{ name: '资源加载', value: nav.resourceLoad, desc: '资源加载时间' },
|
|
9270
|
+
{ name: '总加载时间', value: nav.total, desc: '页面总加载时间' }
|
|
9271
|
+
];
|
|
9272
|
+
|
|
9273
|
+
const tbody = document.getElementById('navigation-metrics');
|
|
9274
|
+
tbody.innerHTML = metrics.map(m => \`
|
|
9275
|
+
<tr>
|
|
9276
|
+
<td><strong>\${m.name}</strong></td>
|
|
9277
|
+
<td>\${Math.round(m.value)} ms</td>
|
|
9278
|
+
<td>\${m.desc}</td>
|
|
9279
|
+
</tr>
|
|
9280
|
+
\`).join('');
|
|
9281
|
+
|
|
9282
|
+
// 更新图表
|
|
9283
|
+
updateNavigationChart(metrics);
|
|
9284
|
+
}
|
|
9285
|
+
|
|
9286
|
+
// 更新导航图表
|
|
9287
|
+
function updateNavigationChart(metrics) {
|
|
9288
|
+
const chart = document.getElementById('navigation-chart');
|
|
9289
|
+
const values = metrics.map(m => m.value).filter(v => v > 0);
|
|
9290
|
+
const maxValue = values.length > 0 ? Math.max(...values) : 1;
|
|
9291
|
+
|
|
9292
|
+
chart.innerHTML = metrics.map(m => {
|
|
9293
|
+
const height = maxValue > 0 ? (m.value / maxValue) * 100 : 0;
|
|
9294
|
+
return \`
|
|
9295
|
+
<div class="chart-bar-item" style="height: \${height}%">
|
|
9296
|
+
<div class="chart-bar-value">\${Math.round(m.value)}ms</div>
|
|
9297
|
+
</div>
|
|
9298
|
+
\`;
|
|
9299
|
+
}).join('') + metrics.map(m => \`
|
|
9300
|
+
<div class="chart-bar-label">\${m.name}</div>
|
|
9301
|
+
\`).join('');
|
|
9302
|
+
}
|
|
9303
|
+
|
|
9304
|
+
// 更新资源标签页
|
|
9305
|
+
function updateResourcesTab() {
|
|
9306
|
+
if (!metricsData) return;
|
|
9307
|
+
|
|
9308
|
+
const resources = metricsData.resources;
|
|
9309
|
+
const tbody = document.getElementById('resources-metrics');
|
|
9310
|
+
|
|
9311
|
+
if (Object.keys(resources.byType).length === 0) {
|
|
9312
|
+
tbody.innerHTML = '<tr><td colspan="4" style="text-align: center; color: #999;">暂无资源数据</td></tr>';
|
|
9313
|
+
return;
|
|
9314
|
+
}
|
|
9315
|
+
|
|
9316
|
+
tbody.innerHTML = Object.entries(resources.byType).map(([type, stats]) => \`
|
|
9317
|
+
<tr>
|
|
9318
|
+
<td><strong>\${type}</strong></td>
|
|
9319
|
+
<td>\${stats.count}</td>
|
|
9320
|
+
<td>\${Math.round(stats.avgLoadTime)} ms</td>
|
|
9321
|
+
<td>\${formatSize(stats.totalSize)}</td>
|
|
9322
|
+
</tr>
|
|
9323
|
+
\`).join('');
|
|
9324
|
+
|
|
9325
|
+
// 添加总计行
|
|
9326
|
+
tbody.innerHTML += \`
|
|
9327
|
+
<tr style="background: #f9f9f9; font-weight: bold;">
|
|
9328
|
+
<td>总计</td>
|
|
9329
|
+
<td>\${resources.total}</td>
|
|
9330
|
+
<td>\${Math.round(resources.avgLoadTime)} ms</td>
|
|
9331
|
+
<td>\${formatSize(resources.totalSize)}</td>
|
|
9332
|
+
</tr>
|
|
9333
|
+
\`;
|
|
9334
|
+
}
|
|
9335
|
+
|
|
9336
|
+
// 更新长任务标签页
|
|
9337
|
+
function updateLongTasksTab() {
|
|
9338
|
+
if (!metricsData) return;
|
|
9339
|
+
|
|
9340
|
+
const longTasks = metricsData.longTasks;
|
|
9341
|
+
const tbody = document.getElementById('longtasks-metrics');
|
|
9342
|
+
|
|
9343
|
+
if (longTasks.recent.length === 0) {
|
|
9344
|
+
tbody.innerHTML = '<tr><td colspan="3" style="text-align: center; color: #999;">暂无长任务数据</td></tr>';
|
|
9345
|
+
return;
|
|
9346
|
+
}
|
|
9347
|
+
|
|
9348
|
+
tbody.innerHTML = longTasks.recent.map(task => {
|
|
9349
|
+
const date = new Date(task.timestamp);
|
|
9350
|
+
return \`
|
|
9351
|
+
<tr>
|
|
9352
|
+
<td>\${date.toLocaleString()}</td>
|
|
9353
|
+
<td>\${Math.round(task.duration)} ms</td>
|
|
9354
|
+
<td>\${Math.round(task.startTime)}</td>
|
|
9355
|
+
</tr>
|
|
9356
|
+
\`;
|
|
9357
|
+
}).join('');
|
|
9358
|
+
}
|
|
9359
|
+
|
|
9360
|
+
// 更新告警标签页
|
|
9361
|
+
function updateAlertsTab() {
|
|
9362
|
+
const container = document.getElementById('alerts-list');
|
|
9363
|
+
|
|
9364
|
+
if (alertsData.length === 0) {
|
|
9365
|
+
container.innerHTML = '<div class="empty-state"><p>暂无告警</p></div>';
|
|
9366
|
+
return;
|
|
9367
|
+
}
|
|
9368
|
+
|
|
9369
|
+
container.innerHTML = alertsData.map(alert => {
|
|
9370
|
+
const date = new Date(alert.timestamp);
|
|
9371
|
+
return \`
|
|
9372
|
+
<div class="alert-item \${alert.level}">
|
|
9373
|
+
<div class="alert-header">
|
|
9374
|
+
<div>
|
|
9375
|
+
<span class="alert-level \${alert.level}">\${alert.level.toUpperCase()}</span>
|
|
9376
|
+
<strong style="margin-left: 10px;">\${alert.message}</strong>
|
|
9377
|
+
</div>
|
|
9378
|
+
<span class="alert-time">\${date.toLocaleString()}</span>
|
|
9379
|
+
</div>
|
|
9380
|
+
<div style="margin-top: 5px; color: #666; font-size: 13px;">
|
|
9381
|
+
值: \${Math.round(alert.value)}ms | 阈值: \${alert.threshold}ms
|
|
9382
|
+
\${alert.url ? ' | URL: ' + alert.url : ''}
|
|
9383
|
+
</div>
|
|
9384
|
+
</div>
|
|
9385
|
+
\`;
|
|
9386
|
+
}).join('');
|
|
9387
|
+
}
|
|
9388
|
+
|
|
9389
|
+
// 切换标签页
|
|
9390
|
+
function switchTab(tabName) {
|
|
9391
|
+
// 更新标签按钮
|
|
9392
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
9393
|
+
tab.classList.remove('active');
|
|
9394
|
+
if (tab.getAttribute('data-tab') === tabName) {
|
|
9395
|
+
tab.classList.add('active');
|
|
9396
|
+
}
|
|
9397
|
+
});
|
|
9398
|
+
|
|
9399
|
+
// 更新内容
|
|
9400
|
+
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
|
9401
|
+
const targetTab = document.getElementById('tab-' + tabName);
|
|
9402
|
+
if (targetTab) {
|
|
9403
|
+
targetTab.classList.add('active');
|
|
9404
|
+
}
|
|
9405
|
+
}
|
|
9406
|
+
|
|
9407
|
+
// 生成报告
|
|
9408
|
+
async function generateReport() {
|
|
9409
|
+
const tracker = getTrackerInstance();
|
|
9410
|
+
if (!tracker) {
|
|
9411
|
+
alert('无法获取Tracker实例');
|
|
9412
|
+
return;
|
|
9413
|
+
}
|
|
9414
|
+
|
|
9415
|
+
try {
|
|
9416
|
+
const report = tracker.generatePerformanceReport({
|
|
9417
|
+
duration: 3600000 // 最近1小时
|
|
9418
|
+
});
|
|
9419
|
+
|
|
9420
|
+
// 下载JSON报告
|
|
9421
|
+
const dataStr = JSON.stringify(report, null, 2);
|
|
9422
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
9423
|
+
const url = URL.createObjectURL(dataBlob);
|
|
9424
|
+
const link = document.createElement('a');
|
|
9425
|
+
link.href = url;
|
|
9426
|
+
link.download = \`performance-report-\${new Date().toISOString().split('T')[0]}.json\`;
|
|
9427
|
+
link.click();
|
|
9428
|
+
URL.revokeObjectURL(url);
|
|
9429
|
+
} catch (error) {
|
|
9430
|
+
console.error('生成报告失败:', error);
|
|
9431
|
+
alert('生成报告失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
|
|
9435
|
+
// 导出数据
|
|
9436
|
+
function exportData() {
|
|
9437
|
+
if (!metricsData) {
|
|
9438
|
+
alert('请先刷新数据');
|
|
9439
|
+
return;
|
|
9440
|
+
}
|
|
9441
|
+
|
|
9442
|
+
const data = {
|
|
9443
|
+
metrics: metricsData,
|
|
9444
|
+
alerts: alertsData,
|
|
9445
|
+
exportedAt: new Date().toISOString()
|
|
9446
|
+
};
|
|
9447
|
+
|
|
9448
|
+
const dataStr = JSON.stringify(data, null, 2);
|
|
9449
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
9450
|
+
const url = URL.createObjectURL(dataBlob);
|
|
9451
|
+
const link = document.createElement('a');
|
|
9452
|
+
link.href = url;
|
|
9453
|
+
link.download = \`performance-data-\${new Date().toISOString().split('T')[0]}.json\`;
|
|
9454
|
+
link.click();
|
|
9455
|
+
URL.revokeObjectURL(url);
|
|
9456
|
+
}
|
|
9457
|
+
|
|
9458
|
+
// 格式化文件大小
|
|
9459
|
+
function formatSize(bytes) {
|
|
9460
|
+
if (bytes === 0) return '0 B';
|
|
9461
|
+
const k = 1024;
|
|
9462
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
9463
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
9464
|
+
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
|
|
9465
|
+
}
|
|
9466
|
+
|
|
9467
|
+
// 页面加载时自动刷新
|
|
9468
|
+
window.addEventListener('load', () => {
|
|
9469
|
+
setTimeout(refreshData, 500);
|
|
9470
|
+
|
|
9471
|
+
// 每30秒自动刷新
|
|
9472
|
+
setInterval(refreshData, 30000);
|
|
9473
|
+
});
|
|
9474
|
+
<\/script>
|
|
9475
|
+
</body>
|
|
9476
|
+
</html>
|
|
9477
|
+
`;
|
|
9478
|
+
const OFFLINE_STORAGE_VIEWER_HTML = `
|
|
9479
|
+
<!DOCTYPE html>
|
|
9480
|
+
<html lang="zh-CN">
|
|
9481
|
+
<head>
|
|
9482
|
+
<meta charset="UTF-8">
|
|
9483
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
9484
|
+
<title>查看离线存储数据 - Tracker SDK</title>
|
|
9485
|
+
<style>
|
|
9486
|
+
* {
|
|
9487
|
+
margin: 0;
|
|
9488
|
+
padding: 0;
|
|
9489
|
+
box-sizing: border-box;
|
|
9490
|
+
}
|
|
9491
|
+
body {
|
|
9492
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
9493
|
+
background: #f5f5f5;
|
|
9494
|
+
padding: 20px;
|
|
9495
|
+
}
|
|
9496
|
+
.container {
|
|
9497
|
+
max-width: 1400px;
|
|
9498
|
+
margin: 0 auto;
|
|
9499
|
+
background: white;
|
|
9500
|
+
border-radius: 8px;
|
|
9501
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
9502
|
+
padding: 30px;
|
|
9503
|
+
}
|
|
9504
|
+
h1 {
|
|
9505
|
+
color: #333;
|
|
9506
|
+
margin-bottom: 10px;
|
|
9507
|
+
border-bottom: 3px solid #4CAF50;
|
|
9508
|
+
padding-bottom: 10px;
|
|
9509
|
+
}
|
|
9510
|
+
.subtitle {
|
|
9511
|
+
color: #666;
|
|
9512
|
+
margin-bottom: 30px;
|
|
9513
|
+
}
|
|
9514
|
+
.toolbar {
|
|
9515
|
+
display: flex;
|
|
9516
|
+
gap: 10px;
|
|
9517
|
+
margin-bottom: 20px;
|
|
9518
|
+
flex-wrap: wrap;
|
|
9519
|
+
}
|
|
9520
|
+
button {
|
|
9521
|
+
background: #4CAF50;
|
|
9522
|
+
color: white;
|
|
9523
|
+
border: none;
|
|
9524
|
+
padding: 10px 20px;
|
|
9525
|
+
border-radius: 4px;
|
|
9526
|
+
cursor: pointer;
|
|
9527
|
+
font-size: 14px;
|
|
9528
|
+
transition: background 0.3s;
|
|
9529
|
+
}
|
|
9530
|
+
button:hover {
|
|
9531
|
+
background: #45a049;
|
|
9532
|
+
}
|
|
9533
|
+
button.danger {
|
|
9534
|
+
background: #f44336;
|
|
9535
|
+
}
|
|
9536
|
+
button.danger:hover {
|
|
9537
|
+
background: #da190b;
|
|
9538
|
+
}
|
|
9539
|
+
button.secondary {
|
|
9540
|
+
background: #2196F3;
|
|
9541
|
+
}
|
|
9542
|
+
button.secondary:hover {
|
|
9543
|
+
background: #1976D2;
|
|
9544
|
+
}
|
|
9545
|
+
.stats {
|
|
9546
|
+
display: grid;
|
|
9547
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
9548
|
+
gap: 15px;
|
|
9549
|
+
margin-bottom: 20px;
|
|
9550
|
+
}
|
|
9551
|
+
.stat-card {
|
|
9552
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
9553
|
+
color: white;
|
|
9554
|
+
padding: 20px;
|
|
9555
|
+
border-radius: 8px;
|
|
9556
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
9557
|
+
}
|
|
9558
|
+
.stat-card.success {
|
|
9559
|
+
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
|
9560
|
+
}
|
|
9561
|
+
.stat-card.warning {
|
|
9562
|
+
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
9563
|
+
}
|
|
9564
|
+
.stat-card.info {
|
|
9565
|
+
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
9566
|
+
}
|
|
9567
|
+
.stat-label {
|
|
9568
|
+
font-size: 12px;
|
|
9569
|
+
opacity: 0.9;
|
|
9570
|
+
margin-bottom: 5px;
|
|
9571
|
+
}
|
|
9572
|
+
.stat-value {
|
|
9573
|
+
font-size: 32px;
|
|
9574
|
+
font-weight: bold;
|
|
9575
|
+
}
|
|
9576
|
+
.filters {
|
|
9577
|
+
background: #f9f9f9;
|
|
9578
|
+
padding: 15px;
|
|
9579
|
+
border-radius: 4px;
|
|
9580
|
+
margin-bottom: 20px;
|
|
9581
|
+
}
|
|
9582
|
+
.filters label {
|
|
9583
|
+
display: inline-block;
|
|
9584
|
+
margin-right: 15px;
|
|
9585
|
+
font-size: 14px;
|
|
9586
|
+
color: #666;
|
|
9587
|
+
}
|
|
9588
|
+
.filters select, .filters input {
|
|
9589
|
+
padding: 5px 10px;
|
|
9590
|
+
border: 1px solid #ddd;
|
|
9591
|
+
border-radius: 4px;
|
|
9592
|
+
margin-left: 5px;
|
|
9593
|
+
}
|
|
9594
|
+
.events-container {
|
|
9595
|
+
max-height: 600px;
|
|
9596
|
+
overflow-y: auto;
|
|
9597
|
+
border: 1px solid #ddd;
|
|
9598
|
+
border-radius: 4px;
|
|
9599
|
+
}
|
|
9600
|
+
.event-item {
|
|
9601
|
+
border-bottom: 1px solid #eee;
|
|
9602
|
+
padding: 15px;
|
|
9603
|
+
transition: background 0.2s;
|
|
9604
|
+
}
|
|
9605
|
+
.event-item:hover {
|
|
9606
|
+
background: #f9f9f9;
|
|
9607
|
+
}
|
|
9608
|
+
.event-item:last-child {
|
|
9609
|
+
border-bottom: none;
|
|
9610
|
+
}
|
|
9611
|
+
.event-header {
|
|
9612
|
+
display: flex;
|
|
9613
|
+
justify-content: space-between;
|
|
9614
|
+
align-items: center;
|
|
9615
|
+
margin-bottom: 10px;
|
|
9616
|
+
}
|
|
9617
|
+
.event-type {
|
|
9618
|
+
display: inline-block;
|
|
9619
|
+
padding: 4px 8px;
|
|
9620
|
+
border-radius: 4px;
|
|
9621
|
+
font-size: 12px;
|
|
9622
|
+
font-weight: bold;
|
|
9623
|
+
color: white;
|
|
9624
|
+
}
|
|
9625
|
+
.event-type.pageview { background: #4CAF50; }
|
|
9626
|
+
.event-type.click { background: #2196F3; }
|
|
9627
|
+
.event-type.exposure { background: #FF9800; }
|
|
9628
|
+
.event-type.error { background: #f44336; }
|
|
9629
|
+
.event-type.performance { background: #9C27B0; }
|
|
9630
|
+
.event-type.custom { background: #607D8B; }
|
|
9631
|
+
.event-time {
|
|
9632
|
+
color: #999;
|
|
9633
|
+
font-size: 12px;
|
|
9634
|
+
}
|
|
9635
|
+
.event-details {
|
|
9636
|
+
font-size: 13px;
|
|
9637
|
+
color: #666;
|
|
9638
|
+
margin-top: 8px;
|
|
9639
|
+
}
|
|
9640
|
+
.event-details pre {
|
|
9641
|
+
background: #f5f5f5;
|
|
9642
|
+
padding: 10px;
|
|
9643
|
+
border-radius: 4px;
|
|
9644
|
+
overflow-x: auto;
|
|
9645
|
+
font-size: 12px;
|
|
9646
|
+
margin-top: 5px;
|
|
9647
|
+
}
|
|
9648
|
+
.empty-state {
|
|
9649
|
+
text-align: center;
|
|
9650
|
+
padding: 60px 20px;
|
|
9651
|
+
color: #999;
|
|
9652
|
+
}
|
|
9653
|
+
.empty-state svg {
|
|
9654
|
+
width: 100px;
|
|
9655
|
+
height: 100px;
|
|
9656
|
+
margin-bottom: 20px;
|
|
9657
|
+
opacity: 0.3;
|
|
9658
|
+
}
|
|
9659
|
+
.loading {
|
|
9660
|
+
text-align: center;
|
|
9661
|
+
padding: 40px;
|
|
9662
|
+
color: #666;
|
|
9663
|
+
}
|
|
9664
|
+
.spinner {
|
|
9665
|
+
border: 3px solid #f3f3f3;
|
|
9666
|
+
border-top: 3px solid #4CAF50;
|
|
9667
|
+
border-radius: 50%;
|
|
9668
|
+
width: 40px;
|
|
9669
|
+
height: 40px;
|
|
9670
|
+
animation: spin 1s linear infinite;
|
|
9671
|
+
margin: 0 auto 20px;
|
|
9672
|
+
}
|
|
9673
|
+
@keyframes spin {
|
|
9674
|
+
0% { transform: rotate(0deg); }
|
|
9675
|
+
100% { transform: rotate(360deg); }
|
|
9676
|
+
}
|
|
9677
|
+
.json-viewer {
|
|
9678
|
+
background: #f5f5f5;
|
|
9679
|
+
padding: 10px;
|
|
9680
|
+
border-radius: 4px;
|
|
9681
|
+
font-family: 'Courier New', monospace;
|
|
9682
|
+
font-size: 12px;
|
|
9683
|
+
max-height: 300px;
|
|
9684
|
+
overflow-y: auto;
|
|
9685
|
+
}
|
|
9686
|
+
</style>
|
|
9687
|
+
</head>
|
|
9688
|
+
<body>
|
|
9689
|
+
<div class="container">
|
|
9690
|
+
<h1>📦 离线存储数据查看器</h1>
|
|
9691
|
+
<p class="subtitle">查看 Tracker SDK 离线存储的所有事件数据</p>
|
|
9692
|
+
|
|
9693
|
+
<div class="toolbar">
|
|
9694
|
+
<button onclick="loadEvents()">🔄 刷新数据</button>
|
|
9695
|
+
<button class="secondary" onclick="exportData()">📥 导出数据</button>
|
|
9696
|
+
<button class="danger" onclick="clearStorage()">🗑️ 清空存储</button>
|
|
9697
|
+
<button onclick="openDevTools()">🔧 打开开发者工具</button>
|
|
9698
|
+
</div>
|
|
9699
|
+
|
|
9700
|
+
<div class="stats" id="stats">
|
|
9701
|
+
<div class="stat-card success">
|
|
9702
|
+
<div class="stat-label">总事件数</div>
|
|
9703
|
+
<div class="stat-value" id="total-count">-</div>
|
|
9704
|
+
</div>
|
|
9705
|
+
<div class="stat-card info">
|
|
9706
|
+
<div class="stat-label">存储大小</div>
|
|
9707
|
+
<div class="stat-value" id="storage-size">-</div>
|
|
9708
|
+
</div>
|
|
9709
|
+
<div class="stat-card warning">
|
|
9710
|
+
<div class="stat-label">事件类型分布</div>
|
|
9711
|
+
<div class="stat-value" id="type-distribution">-</div>
|
|
9712
|
+
</div>
|
|
9713
|
+
</div>
|
|
9714
|
+
|
|
9715
|
+
<div class="filters">
|
|
9716
|
+
<label>
|
|
9717
|
+
事件类型:
|
|
9718
|
+
<select id="filter-type" onchange="filterEvents()">
|
|
9719
|
+
<option value="">全部</option>
|
|
9720
|
+
<option value="pageview">页面浏览</option>
|
|
9721
|
+
<option value="click">点击</option>
|
|
9722
|
+
<option value="exposure">曝光</option>
|
|
9723
|
+
<option value="error">错误</option>
|
|
9724
|
+
<option value="performance">性能</option>
|
|
9725
|
+
<option value="custom">自定义</option>
|
|
9726
|
+
</select>
|
|
9727
|
+
</label>
|
|
9728
|
+
<label>
|
|
9729
|
+
搜索:
|
|
9730
|
+
<input type="text" id="search-input" placeholder="搜索事件..." oninput="filterEvents()">
|
|
9731
|
+
</label>
|
|
9732
|
+
<label>
|
|
9733
|
+
排序:
|
|
9734
|
+
<select id="sort-order" onchange="filterEvents()">
|
|
9735
|
+
<option value="desc">最新优先</option>
|
|
9736
|
+
<option value="asc">最旧优先</option>
|
|
9737
|
+
</select>
|
|
9738
|
+
</label>
|
|
9739
|
+
</div>
|
|
9740
|
+
|
|
9741
|
+
<div class="events-container" id="events-container">
|
|
9742
|
+
<div class="loading">
|
|
9743
|
+
<div class="spinner"></div>
|
|
9744
|
+
<p>加载中...</p>
|
|
9745
|
+
</div>
|
|
9746
|
+
</div>
|
|
9747
|
+
</div>
|
|
9748
|
+
|
|
9749
|
+
<script>
|
|
9750
|
+
// 标记这是SDK内部面板,不应被监控
|
|
9751
|
+
window.__trackerInternalPanel = true;
|
|
9752
|
+
|
|
9753
|
+
let allEvents = [];
|
|
9754
|
+
let filteredEvents = [];
|
|
9755
|
+
|
|
9756
|
+
// 从父窗口获取Tracker实例
|
|
9757
|
+
function getTrackerInstance() {
|
|
9758
|
+
if (window.opener && window.opener.__trackerInstance) {
|
|
9759
|
+
return window.opener.__trackerInstance;
|
|
9760
|
+
}
|
|
9761
|
+
// 尝试从全局变量获取
|
|
9762
|
+
if (window.__trackerInstance) {
|
|
9763
|
+
return window.__trackerInstance;
|
|
9764
|
+
}
|
|
9765
|
+
return null;
|
|
9766
|
+
}
|
|
9767
|
+
|
|
9768
|
+
// 加载事件
|
|
9769
|
+
async function loadEvents() {
|
|
9770
|
+
const container = document.getElementById('events-container');
|
|
9771
|
+
container.innerHTML = '<div class="loading"><div class="spinner"></div><p>加载中...</p></div>';
|
|
9772
|
+
|
|
9773
|
+
try {
|
|
9774
|
+
const tracker = getTrackerInstance();
|
|
9775
|
+
if (tracker) {
|
|
9776
|
+
allEvents = await tracker.getOfflineEvents();
|
|
9777
|
+
} else {
|
|
9778
|
+
// 直接访问IndexedDB
|
|
9779
|
+
allEvents = await getEventsFromIndexedDB();
|
|
9780
|
+
}
|
|
9781
|
+
|
|
9782
|
+
updateStats();
|
|
9783
|
+
filterEvents();
|
|
9784
|
+
} catch (error) {
|
|
9785
|
+
container.innerHTML = \`<div class="empty-state"><p>加载失败: \${error.message}</p></div>\`;
|
|
9786
|
+
console.error('加载事件失败:', error);
|
|
9787
|
+
}
|
|
9788
|
+
}
|
|
9789
|
+
|
|
9790
|
+
// 直接从IndexedDB获取事件
|
|
9791
|
+
async function getEventsFromIndexedDB() {
|
|
9792
|
+
return new Promise((resolve, reject) => {
|
|
9793
|
+
const request = indexedDB.open('__tracker_db__', 1);
|
|
9794
|
+
|
|
9795
|
+
request.onsuccess = (event) => {
|
|
9796
|
+
const db = event.target.result;
|
|
9797
|
+
if (!db.objectStoreNames.contains('__tracker_store__')) {
|
|
9798
|
+
resolve([]);
|
|
9799
|
+
return;
|
|
9800
|
+
}
|
|
9801
|
+
const transaction = db.transaction(['__tracker_store__'], 'readonly');
|
|
9802
|
+
const store = transaction.objectStore('__tracker_store__');
|
|
9803
|
+
const index = store.index('key');
|
|
9804
|
+
const getRequest = index.get('__tracker_offline_events__');
|
|
9805
|
+
|
|
9806
|
+
getRequest.onsuccess = () => {
|
|
9807
|
+
const result = getRequest.result;
|
|
9808
|
+
if (result && result.value) {
|
|
9809
|
+
try {
|
|
9810
|
+
const events = JSON.parse(result.value);
|
|
9811
|
+
resolve(events);
|
|
9812
|
+
} catch (e) {
|
|
9813
|
+
resolve([]);
|
|
9814
|
+
}
|
|
9815
|
+
} else {
|
|
9816
|
+
resolve([]);
|
|
9817
|
+
}
|
|
9818
|
+
};
|
|
9819
|
+
|
|
9820
|
+
getRequest.onerror = () => {
|
|
9821
|
+
reject(new Error('获取数据失败'));
|
|
9822
|
+
};
|
|
9823
|
+
};
|
|
9824
|
+
|
|
9825
|
+
request.onerror = () => {
|
|
9826
|
+
reject(new Error('打开数据库失败'));
|
|
9827
|
+
};
|
|
9828
|
+
});
|
|
9829
|
+
}
|
|
9830
|
+
|
|
9831
|
+
// 更新统计信息
|
|
9832
|
+
function updateStats() {
|
|
9833
|
+
const totalCount = allEvents.length;
|
|
9834
|
+
document.getElementById('total-count').textContent = totalCount;
|
|
9835
|
+
|
|
9836
|
+
const size = new Blob([JSON.stringify(allEvents)]).size;
|
|
9837
|
+
const sizeKB = (size / 1024).toFixed(2);
|
|
9838
|
+
document.getElementById('storage-size').textContent = sizeKB + ' KB';
|
|
9839
|
+
|
|
9840
|
+
const typeCount = {};
|
|
9841
|
+
allEvents.forEach(event => {
|
|
9842
|
+
typeCount[event.type] = (typeCount[event.type] || 0) + 1;
|
|
9843
|
+
});
|
|
9844
|
+
const typeStr = Object.entries(typeCount)
|
|
9845
|
+
.map(([type, count]) => \`\${type}: \${count}\`)
|
|
9846
|
+
.join(', ');
|
|
9847
|
+
document.getElementById('type-distribution').textContent = typeStr || '无';
|
|
9848
|
+
}
|
|
9849
|
+
|
|
9850
|
+
// 过滤事件
|
|
9851
|
+
function filterEvents() {
|
|
9852
|
+
const typeFilter = document.getElementById('filter-type').value;
|
|
9853
|
+
const searchText = document.getElementById('search-input').value.toLowerCase();
|
|
9854
|
+
const sortOrder = document.getElementById('sort-order').value;
|
|
9855
|
+
|
|
9856
|
+
filteredEvents = allEvents.filter(event => {
|
|
9857
|
+
const matchType = !typeFilter || event.type === typeFilter;
|
|
9858
|
+
const matchSearch = !searchText ||
|
|
9859
|
+
JSON.stringify(event).toLowerCase().includes(searchText);
|
|
9860
|
+
return matchType && matchSearch;
|
|
9861
|
+
});
|
|
9862
|
+
|
|
9863
|
+
filteredEvents.sort((a, b) => {
|
|
9864
|
+
return sortOrder === 'desc' ? b.timestamp - a.timestamp : a.timestamp - b.timestamp;
|
|
9865
|
+
});
|
|
9866
|
+
|
|
9867
|
+
renderEvents();
|
|
9868
|
+
}
|
|
9869
|
+
|
|
9870
|
+
// 渲染事件列表
|
|
9871
|
+
function renderEvents() {
|
|
9872
|
+
const container = document.getElementById('events-container');
|
|
9873
|
+
|
|
9874
|
+
if (filteredEvents.length === 0) {
|
|
9875
|
+
container.innerHTML = \`
|
|
9876
|
+
<div class="empty-state">
|
|
9877
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
9878
|
+
<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
|
9879
|
+
</svg>
|
|
9880
|
+
<p>没有找到事件数据</p>
|
|
9881
|
+
<p style="margin-top: 10px; font-size: 12px;">请确保已经使用Tracker SDK收集了一些事件</p>
|
|
9882
|
+
</div>
|
|
9883
|
+
\`;
|
|
9884
|
+
return;
|
|
9885
|
+
}
|
|
9886
|
+
|
|
9887
|
+
const typeLabels = {
|
|
9888
|
+
pageview: '页面浏览',
|
|
9889
|
+
click: '点击',
|
|
9890
|
+
exposure: '曝光',
|
|
9891
|
+
error: '错误',
|
|
9892
|
+
performance: '性能',
|
|
9893
|
+
custom: '自定义'
|
|
9894
|
+
};
|
|
9895
|
+
|
|
9896
|
+
container.innerHTML = filteredEvents.map((event) => {
|
|
9897
|
+
const date = new Date(event.timestamp);
|
|
9898
|
+
return \`
|
|
9899
|
+
<div class="event-item">
|
|
9900
|
+
<div class="event-header">
|
|
9901
|
+
<div>
|
|
9902
|
+
<span class="event-type \${event.type}">\${typeLabels[event.type] || event.type}</span>
|
|
9903
|
+
<span style="margin-left: 10px; font-weight: bold;">\${event.name || event.eventName || '-'}</span>
|
|
9904
|
+
</div>
|
|
9905
|
+
<div class="event-time">\${date.toLocaleString()}</div>
|
|
9906
|
+
</div>
|
|
9907
|
+
<div class="event-details">
|
|
9908
|
+
<div><strong>URL:</strong> \${event.url || '-'}</div>
|
|
9909
|
+
<div style="margin-top: 5px;"><strong>标题:</strong> \${event.title || '-'}</div>
|
|
9910
|
+
\${event.userId ? \`<div style="margin-top: 5px;"><strong>用户ID:</strong> \${event.userId}</div>\` : ''}
|
|
9911
|
+
\${event.properties ? \`
|
|
9912
|
+
<div style="margin-top: 10px;">
|
|
9913
|
+
<strong>属性:</strong>
|
|
9914
|
+
<div class="json-viewer">\${JSON.stringify(event.properties, null, 2)}</div>
|
|
9915
|
+
</div>
|
|
9916
|
+
\` : ''}
|
|
9917
|
+
\${event.errorType ? \`<div style="margin-top: 5px;"><strong>错误类型:</strong> \${event.errorType}</div>\` : ''}
|
|
9918
|
+
\${event.message ? \`<div style="margin-top: 5px;"><strong>消息:</strong> \${event.message}</div>\` : ''}
|
|
9919
|
+
<div style="margin-top: 10px; font-size: 11px; color: #999;">
|
|
9920
|
+
<details>
|
|
9921
|
+
<summary style="cursor: pointer;">查看完整数据</summary>
|
|
9922
|
+
<div class="json-viewer" style="margin-top: 10px;">\${JSON.stringify(event, null, 2)}</div>
|
|
9923
|
+
</details>
|
|
9924
|
+
</div>
|
|
9925
|
+
</div>
|
|
9926
|
+
</div>
|
|
9927
|
+
\`;
|
|
9928
|
+
}).join('');
|
|
9929
|
+
}
|
|
9930
|
+
|
|
9931
|
+
// 导出数据
|
|
9932
|
+
function exportData() {
|
|
9933
|
+
const dataStr = JSON.stringify(allEvents, null, 2);
|
|
9934
|
+
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
|
9935
|
+
const url = URL.createObjectURL(dataBlob);
|
|
9936
|
+
const link = document.createElement('a');
|
|
9937
|
+
link.href = url;
|
|
9938
|
+
link.download = \`tracker-offline-events-\${new Date().toISOString().split('T')[0]}.json\`;
|
|
9939
|
+
link.click();
|
|
9940
|
+
URL.revokeObjectURL(url);
|
|
9941
|
+
}
|
|
9942
|
+
|
|
9943
|
+
// 清空存储
|
|
9944
|
+
async function clearStorage() {
|
|
9945
|
+
if (!confirm('确定要清空所有离线存储的数据吗?此操作不可恢复!')) {
|
|
9946
|
+
return;
|
|
9947
|
+
}
|
|
9948
|
+
|
|
9949
|
+
try {
|
|
9950
|
+
const tracker = getTrackerInstance();
|
|
9951
|
+
if (tracker) {
|
|
9952
|
+
await tracker.clearOfflineStorage();
|
|
9953
|
+
} else {
|
|
9954
|
+
const request = indexedDB.open('__tracker_db__', 1);
|
|
9955
|
+
request.onsuccess = (event) => {
|
|
9956
|
+
const db = event.target.result;
|
|
9957
|
+
if (!db.objectStoreNames.contains('__tracker_store__')) {
|
|
9958
|
+
alert('存储已清空(存储不存在)');
|
|
9959
|
+
loadEvents();
|
|
9960
|
+
return;
|
|
9961
|
+
}
|
|
9962
|
+
const transaction = db.transaction(['__tracker_store__'], 'readwrite');
|
|
9963
|
+
const store = transaction.objectStore('__tracker_store__');
|
|
9964
|
+
const index = store.index('key');
|
|
9965
|
+
const getRequest = index.get('__tracker_offline_events__');
|
|
9966
|
+
getRequest.onsuccess = () => {
|
|
9967
|
+
if (getRequest.result) {
|
|
9968
|
+
store.delete(getRequest.result.id);
|
|
9969
|
+
}
|
|
9970
|
+
};
|
|
9971
|
+
};
|
|
9972
|
+
}
|
|
9973
|
+
alert('存储已清空');
|
|
9974
|
+
loadEvents();
|
|
9975
|
+
} catch (error) {
|
|
9976
|
+
alert('清空失败: ' + error.message);
|
|
9977
|
+
console.error('清空存储失败:', error);
|
|
9978
|
+
}
|
|
9979
|
+
}
|
|
9980
|
+
|
|
9981
|
+
// 打开开发者工具
|
|
9982
|
+
function openDevTools() {
|
|
9983
|
+
alert('请在浏览器中按F12打开开发者工具,然后:\\n\\n1. 打开 Application 标签\\n2. 选择 IndexedDB\\n3. 找到 TrackerStorage 数据库\\n4. 查看 keyValueStore 中的数据');
|
|
9984
|
+
}
|
|
9985
|
+
|
|
9986
|
+
// 页面加载时自动加载数据
|
|
9987
|
+
window.addEventListener('load', () => {
|
|
9988
|
+
setTimeout(loadEvents, 500);
|
|
9989
|
+
});
|
|
9990
|
+
<\/script>
|
|
9991
|
+
</body>
|
|
9992
|
+
</html>
|
|
9993
|
+
`;
|
|
9994
|
+
class PanelManager {
|
|
9995
|
+
/**
|
|
9996
|
+
* 设置Tracker实例
|
|
9997
|
+
*/
|
|
9998
|
+
setTrackerInstance(instance) {
|
|
9999
|
+
if (typeof window !== "undefined") {
|
|
10000
|
+
window.__trackerInstance = instance;
|
|
10001
|
+
}
|
|
10002
|
+
}
|
|
10003
|
+
/**
|
|
10004
|
+
* 打开性能监控面板
|
|
10005
|
+
*/
|
|
10006
|
+
openPerformancePanel() {
|
|
10007
|
+
if (typeof window === "undefined") {
|
|
10008
|
+
console.warn("PanelManager: Cannot open panel in non-browser environment");
|
|
10009
|
+
return;
|
|
10010
|
+
}
|
|
10011
|
+
try {
|
|
10012
|
+
const win = window.open("about:blank", "_blank", "width=1400,height=900,resizable=yes,scrollbars=yes");
|
|
10013
|
+
if (!win) {
|
|
10014
|
+
alert("无法打开性能监控面板,请检查浏览器弹窗设置");
|
|
10015
|
+
return;
|
|
10016
|
+
}
|
|
10017
|
+
win.document.write(PERFORMANCE_PANEL_HTML);
|
|
10018
|
+
win.document.close();
|
|
10019
|
+
win.document.title = "性能监控面板 - Tracker SDK";
|
|
10020
|
+
} catch (error) {
|
|
10021
|
+
console.error("PanelManager: Failed to open performance panel:", error);
|
|
10022
|
+
alert("打开性能监控面板失败: " + (error instanceof Error ? error.message : String(error)));
|
|
10023
|
+
}
|
|
10024
|
+
}
|
|
10025
|
+
/**
|
|
10026
|
+
* 打开离线存储查看器
|
|
10027
|
+
*/
|
|
10028
|
+
openOfflineStorageViewer() {
|
|
10029
|
+
if (typeof window === "undefined") {
|
|
10030
|
+
console.warn("PanelManager: Cannot open panel in non-browser environment");
|
|
10031
|
+
return;
|
|
10032
|
+
}
|
|
10033
|
+
try {
|
|
10034
|
+
const win = window.open("about:blank", "_blank", "width=1400,height=900,resizable=yes,scrollbars=yes");
|
|
10035
|
+
if (!win) {
|
|
10036
|
+
alert("无法打开离线存储查看器,请检查浏览器弹窗设置");
|
|
10037
|
+
return;
|
|
10038
|
+
}
|
|
10039
|
+
win.document.write(OFFLINE_STORAGE_VIEWER_HTML);
|
|
10040
|
+
win.document.close();
|
|
10041
|
+
win.document.title = "离线存储数据查看器 - Tracker SDK";
|
|
10042
|
+
} catch (error) {
|
|
10043
|
+
console.error("PanelManager: Failed to open offline storage viewer:", error);
|
|
10044
|
+
alert("打开离线存储查看器失败: " + (error instanceof Error ? error.message : String(error)));
|
|
10045
|
+
}
|
|
10046
|
+
}
|
|
10047
|
+
/**
|
|
10048
|
+
* 获取性能监控面板HTML
|
|
10049
|
+
*/
|
|
10050
|
+
getPerformancePanelHTML() {
|
|
10051
|
+
return PERFORMANCE_PANEL_HTML;
|
|
10052
|
+
}
|
|
10053
|
+
/**
|
|
10054
|
+
* 获取离线存储查看器HTML
|
|
10055
|
+
*/
|
|
10056
|
+
getOfflineStorageViewerHTML() {
|
|
10057
|
+
return OFFLINE_STORAGE_VIEWER_HTML;
|
|
10058
|
+
}
|
|
10059
|
+
}
|
|
10060
|
+
class Tracker {
|
|
10061
|
+
constructor(config) {
|
|
10062
|
+
__publicField(this, "config");
|
|
10063
|
+
__publicField(this, "autoTracker", null);
|
|
10064
|
+
__publicField(this, "errorTracker", null);
|
|
10065
|
+
__publicField(this, "performanceTracker", null);
|
|
10066
|
+
__publicField(this, "customTracker");
|
|
10067
|
+
__publicField(this, "dataProcessor");
|
|
10068
|
+
__publicField(this, "batchSender");
|
|
10069
|
+
__publicField(this, "pluginManager");
|
|
10070
|
+
__publicField(this, "behaviorAnalysis");
|
|
10071
|
+
__publicField(this, "sessionReplay", null);
|
|
10072
|
+
__publicField(this, "performanceMonitor");
|
|
10073
|
+
__publicField(this, "panelManager");
|
|
10074
|
+
__publicField(this, "userInfo", null);
|
|
10075
|
+
__publicField(this, "deviceId");
|
|
10076
|
+
__publicField(this, "sessionId");
|
|
10077
|
+
if (!config.appId) {
|
|
10078
|
+
throw new Error("Tracker: appId is required");
|
|
10079
|
+
}
|
|
10080
|
+
this.config = new ConfigManager(config);
|
|
10081
|
+
this.deviceId = config.deviceId || getDeviceId();
|
|
10082
|
+
this.sessionId = getSessionId();
|
|
10083
|
+
this.customTracker = new CustomTracker();
|
|
10084
|
+
this.dataProcessor = new DataProcessor();
|
|
10085
|
+
const serverUrl = config.serverUrl || "about:blank";
|
|
10086
|
+
this.batchSender = new BatchSender(serverUrl, {
|
|
10087
|
+
batchSize: this.config.get("batchSize"),
|
|
10088
|
+
batchWait: this.config.get("batchWait"),
|
|
10089
|
+
enableOfflineStorage: this.config.get("enableOfflineStorage") ?? true,
|
|
10090
|
+
// 未设置serverUrl时强制启用离线存储
|
|
10091
|
+
maxOfflineStorageSize: this.config.get("maxOfflineStorageSize")
|
|
10092
|
+
});
|
|
10093
|
+
if (!config.serverUrl) {
|
|
10094
|
+
this.batchSender.setServerUrl("");
|
|
10095
|
+
}
|
|
10096
|
+
this.pluginManager = new PluginManager();
|
|
10097
|
+
this.behaviorAnalysis = new BehaviorAnalysis();
|
|
10098
|
+
this.performanceMonitor = new PerformanceMonitor();
|
|
10099
|
+
this.panelManager = new PanelManager();
|
|
10100
|
+
this.pluginManager.setTrackerInstance({
|
|
10101
|
+
send: this.send.bind(this),
|
|
10102
|
+
getConfig: () => this.config.getAll(),
|
|
10103
|
+
setUser: this.setUser.bind(this),
|
|
10104
|
+
getUser: () => this.userInfo
|
|
10105
|
+
});
|
|
10106
|
+
this.panelManager.setTrackerInstance(this);
|
|
10107
|
+
this.init();
|
|
10108
|
+
if (typeof window !== "undefined") {
|
|
10109
|
+
window.__trackerEventBus = {
|
|
10110
|
+
emit: (event, data) => {
|
|
10111
|
+
eventBus.emit(event, data);
|
|
10112
|
+
}
|
|
10113
|
+
};
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
10116
|
+
/**
|
|
10117
|
+
* 初始化
|
|
10118
|
+
*/
|
|
10119
|
+
init() {
|
|
10120
|
+
if (this.config.get("enableAutoTrack")) {
|
|
10121
|
+
this.autoTracker = new AutoTracker();
|
|
10122
|
+
this.autoTracker.init({
|
|
10123
|
+
enableVueTracking: this.config.get("enableVueTracking") ?? false,
|
|
10124
|
+
enableNaiveUITracking: this.config.get("enableNaiveUITracking") ?? false,
|
|
10125
|
+
extractComponentProps: this.config.get("extractComponentProps") ?? true,
|
|
10126
|
+
extractComponentValue: this.config.get("extractComponentValue") ?? true
|
|
10127
|
+
});
|
|
10128
|
+
this.setupAutoTrackListeners();
|
|
10129
|
+
}
|
|
10130
|
+
if (this.config.get("enableErrorTrack")) {
|
|
10131
|
+
this.errorTracker = new ErrorTracker();
|
|
10132
|
+
this.errorTracker.init();
|
|
10133
|
+
}
|
|
10134
|
+
if (this.config.get("enablePerformanceTrack")) {
|
|
10135
|
+
this.performanceTracker = new PerformanceTracker();
|
|
10136
|
+
this.performanceTracker.init();
|
|
10137
|
+
}
|
|
10138
|
+
if (this.config.get("enableSessionReplay")) {
|
|
10139
|
+
this.sessionReplay = new SessionReplay();
|
|
10140
|
+
this.sessionReplay.start();
|
|
10141
|
+
}
|
|
10142
|
+
if (this.config.get("userId")) {
|
|
10143
|
+
this.setUser({ userId: this.config.get("userId") });
|
|
10144
|
+
}
|
|
10145
|
+
}
|
|
10146
|
+
/**
|
|
10147
|
+
* 设置自动埋点监听器
|
|
10148
|
+
*/
|
|
10149
|
+
setupAutoTrackListeners() {
|
|
10150
|
+
eventBus.on("pageview", (event) => {
|
|
10151
|
+
this.send(event);
|
|
10152
|
+
});
|
|
10153
|
+
eventBus.on("click", (event) => {
|
|
10154
|
+
this.send(event);
|
|
10155
|
+
});
|
|
10156
|
+
eventBus.on("exposure", (event) => {
|
|
10157
|
+
this.send(event);
|
|
10158
|
+
});
|
|
10159
|
+
eventBus.on("input", (event) => {
|
|
10160
|
+
this.send(event);
|
|
10161
|
+
});
|
|
10162
|
+
eventBus.on("error", (event) => {
|
|
10163
|
+
this.send(event);
|
|
10164
|
+
});
|
|
10165
|
+
eventBus.on("performance", (event) => {
|
|
10166
|
+
this.send(event);
|
|
10167
|
+
});
|
|
10168
|
+
}
|
|
10169
|
+
/**
|
|
10170
|
+
* 发送事件
|
|
10171
|
+
*/
|
|
10172
|
+
send(event) {
|
|
10173
|
+
if (typeof window !== "undefined" && isTrackerInternalPanel()) {
|
|
10174
|
+
return;
|
|
10175
|
+
}
|
|
10176
|
+
try {
|
|
10177
|
+
const enrichedEvent = this.enrichEvent(event);
|
|
10178
|
+
const processedEvent = this.dataProcessor.process(enrichedEvent, {
|
|
10179
|
+
sampleRate: this.config.get("sampleRate"),
|
|
10180
|
+
enableDeduplication: true,
|
|
10181
|
+
isCritical: event.type === "error"
|
|
10182
|
+
});
|
|
10183
|
+
if (!processedEvent) {
|
|
10184
|
+
return;
|
|
10185
|
+
}
|
|
10186
|
+
const beforeSend = this.config.get("beforeSend");
|
|
10187
|
+
if (beforeSend) {
|
|
10188
|
+
const result = beforeSend(processedEvent);
|
|
10189
|
+
if (result === false || result === null) {
|
|
10190
|
+
return;
|
|
10191
|
+
}
|
|
10192
|
+
if (result) {
|
|
10193
|
+
Object.assign(processedEvent, result);
|
|
10194
|
+
}
|
|
10195
|
+
}
|
|
10196
|
+
this.batchSender.add(processedEvent, {
|
|
10197
|
+
onBatchComplete: (events, success) => {
|
|
10198
|
+
const afterSend = this.config.get("afterSend");
|
|
10199
|
+
if (afterSend) {
|
|
10200
|
+
events.forEach((event2) => {
|
|
10201
|
+
setTimeout(() => {
|
|
10202
|
+
afterSend(event2, success);
|
|
10203
|
+
}, 0);
|
|
10204
|
+
});
|
|
10205
|
+
}
|
|
10206
|
+
}
|
|
10207
|
+
});
|
|
10208
|
+
if (event.type === "pageview") {
|
|
10209
|
+
this.behaviorAnalysis.analyzePageView(processedEvent);
|
|
10210
|
+
}
|
|
10211
|
+
} catch (error) {
|
|
10212
|
+
const onError = this.config.get("onError");
|
|
10213
|
+
if (onError) {
|
|
10214
|
+
onError(error);
|
|
10215
|
+
} else {
|
|
10216
|
+
console.error("Tracker: Error sending event:", error);
|
|
10217
|
+
}
|
|
10218
|
+
}
|
|
10219
|
+
}
|
|
10220
|
+
/**
|
|
10221
|
+
* 丰富事件数据
|
|
10222
|
+
*/
|
|
10223
|
+
enrichEvent(event) {
|
|
10224
|
+
var _a;
|
|
10225
|
+
const userId = event.userId || ((_a = this.userInfo) == null ? void 0 : _a.userId) || this.config.get("userId");
|
|
10226
|
+
const customProperties = this.config.get("customProperties") || {};
|
|
10227
|
+
const eventProperties = event.properties || {};
|
|
10228
|
+
const mergedProperties = {
|
|
10229
|
+
...customProperties,
|
|
10230
|
+
...eventProperties
|
|
10231
|
+
};
|
|
10232
|
+
const finalProperties = Object.keys(mergedProperties).length > 0 ? mergedProperties : void 0;
|
|
10233
|
+
const enriched = {
|
|
10234
|
+
...event,
|
|
10235
|
+
deviceId: event.deviceId || this.deviceId,
|
|
10236
|
+
sessionId: event.sessionId || this.sessionId
|
|
10237
|
+
};
|
|
10238
|
+
if (userId && userId !== "") {
|
|
10239
|
+
enriched.userId = userId;
|
|
10240
|
+
}
|
|
10241
|
+
if (finalProperties) {
|
|
10242
|
+
enriched.properties = finalProperties;
|
|
10243
|
+
}
|
|
10244
|
+
return enriched;
|
|
10245
|
+
}
|
|
10246
|
+
/**
|
|
10247
|
+
* 追踪自定义事件
|
|
10248
|
+
*/
|
|
10249
|
+
track(eventName, properties) {
|
|
10250
|
+
const event = this.customTracker.track(eventName, properties);
|
|
10251
|
+
this.send(event);
|
|
10252
|
+
}
|
|
10253
|
+
/**
|
|
10254
|
+
* 设置用户信息
|
|
10255
|
+
*/
|
|
10256
|
+
setUser(user) {
|
|
10257
|
+
this.userInfo = { ...this.userInfo, ...user };
|
|
10258
|
+
this.config.set("userId", user.userId || "");
|
|
7817
10259
|
}
|
|
7818
10260
|
/**
|
|
7819
10261
|
* 获取用户信息
|
|
@@ -7887,6 +10329,36 @@ class Tracker {
|
|
|
7887
10329
|
getConfig() {
|
|
7888
10330
|
return this.config.getAll();
|
|
7889
10331
|
}
|
|
10332
|
+
/**
|
|
10333
|
+
* 打开性能监控面板
|
|
10334
|
+
*/
|
|
10335
|
+
openPerformancePanel() {
|
|
10336
|
+
this.panelManager.openPerformancePanel();
|
|
10337
|
+
}
|
|
10338
|
+
/**
|
|
10339
|
+
* 打开离线存储查看器
|
|
10340
|
+
*/
|
|
10341
|
+
openOfflineStorageViewer() {
|
|
10342
|
+
this.panelManager.openOfflineStorageViewer();
|
|
10343
|
+
}
|
|
10344
|
+
/**
|
|
10345
|
+
* 获取性能监控数据
|
|
10346
|
+
*/
|
|
10347
|
+
getPerformanceMetrics(startTime, endTime) {
|
|
10348
|
+
return this.performanceMonitor.getMetrics(startTime, endTime);
|
|
10349
|
+
}
|
|
10350
|
+
/**
|
|
10351
|
+
* 生成性能报告
|
|
10352
|
+
*/
|
|
10353
|
+
generatePerformanceReport(options) {
|
|
10354
|
+
return this.performanceMonitor.generateReport(options);
|
|
10355
|
+
}
|
|
10356
|
+
/**
|
|
10357
|
+
* 获取性能监控器实例
|
|
10358
|
+
*/
|
|
10359
|
+
getPerformanceMonitor() {
|
|
10360
|
+
return this.performanceMonitor;
|
|
10361
|
+
}
|
|
7890
10362
|
/**
|
|
7891
10363
|
* 获取行为分析数据
|
|
7892
10364
|
*/
|
|
@@ -7934,6 +10406,7 @@ class Tracker {
|
|
|
7934
10406
|
(_b = this.errorTracker) == null ? void 0 : _b.destroy();
|
|
7935
10407
|
(_c = this.performanceTracker) == null ? void 0 : _c.destroy();
|
|
7936
10408
|
(_d = this.sessionReplay) == null ? void 0 : _d.destroy();
|
|
10409
|
+
this.performanceMonitor.destroy();
|
|
7937
10410
|
this.batchSender.destroy();
|
|
7938
10411
|
this.pluginManager.clear();
|
|
7939
10412
|
eventBus.clear();
|
|
@@ -7947,6 +10420,7 @@ class Tracker {
|
|
|
7947
10420
|
(_b = this.errorTracker) == null ? void 0 : _b.destroy();
|
|
7948
10421
|
(_c = this.performanceTracker) == null ? void 0 : _c.destroy();
|
|
7949
10422
|
(_d = this.sessionReplay) == null ? void 0 : _d.destroy();
|
|
10423
|
+
this.performanceMonitor.destroy();
|
|
7950
10424
|
await this.batchSender.destroyAsync();
|
|
7951
10425
|
this.pluginManager.clear();
|
|
7952
10426
|
eventBus.clear();
|
|
@@ -8430,14 +10904,587 @@ function createNetworkInterceptorPlugin(options = {}) {
|
|
|
8430
10904
|
};
|
|
8431
10905
|
}
|
|
8432
10906
|
const networkInterceptorPlugin = createNetworkInterceptorPlugin();
|
|
10907
|
+
function vuePlugin(app, options) {
|
|
10908
|
+
const { tracker, enableAutoTrack = true, enableComponentInfo = true } = options;
|
|
10909
|
+
if (!tracker) {
|
|
10910
|
+
console.error("VuePlugin: Tracker instance is required");
|
|
10911
|
+
return;
|
|
10912
|
+
}
|
|
10913
|
+
app.config.globalProperties.$tracker = tracker;
|
|
10914
|
+
app.provide("tracker", tracker);
|
|
10915
|
+
if (enableAutoTrack) {
|
|
10916
|
+
app.mixin({
|
|
10917
|
+
mounted() {
|
|
10918
|
+
}
|
|
10919
|
+
});
|
|
10920
|
+
}
|
|
10921
|
+
}
|
|
10922
|
+
function useTracker() {
|
|
10923
|
+
var _a, _b;
|
|
10924
|
+
if (typeof window === "undefined") {
|
|
10925
|
+
return void 0;
|
|
10926
|
+
}
|
|
10927
|
+
const vueApp = window.__VUE_APP__;
|
|
10928
|
+
if ((_b = (_a = vueApp == null ? void 0 : vueApp.config) == null ? void 0 : _a.globalProperties) == null ? void 0 : _b.$tracker) {
|
|
10929
|
+
return vueApp.config.globalProperties.$tracker;
|
|
10930
|
+
}
|
|
10931
|
+
const globalTracker = window.__TRACKER_INSTANCE__;
|
|
10932
|
+
if (globalTracker) {
|
|
10933
|
+
return globalTracker;
|
|
10934
|
+
}
|
|
10935
|
+
return void 0;
|
|
10936
|
+
}
|
|
10937
|
+
class UserActionSequence {
|
|
10938
|
+
constructor(sessionId, deviceId, userId, options = {}) {
|
|
10939
|
+
__publicField(this, "events", []);
|
|
10940
|
+
__publicField(this, "sessionId");
|
|
10941
|
+
__publicField(this, "userId");
|
|
10942
|
+
__publicField(this, "deviceId");
|
|
10943
|
+
__publicField(this, "startTime");
|
|
10944
|
+
__publicField(this, "maxEvents");
|
|
10945
|
+
__publicField(this, "sampleRate");
|
|
10946
|
+
__publicField(this, "enableSampling");
|
|
10947
|
+
__publicField(this, "performanceMode");
|
|
10948
|
+
__publicField(this, "performanceBudget");
|
|
10949
|
+
// 索引优化
|
|
10950
|
+
__publicField(this, "eventTypeCounts", /* @__PURE__ */ new Map());
|
|
10951
|
+
__publicField(this, "isSorted", true);
|
|
10952
|
+
__publicField(this, "lastEventTimestamp", 0);
|
|
10953
|
+
__publicField(this, "eventIndexes", {
|
|
10954
|
+
pageViews: [],
|
|
10955
|
+
clicks: [],
|
|
10956
|
+
inputs: [],
|
|
10957
|
+
errors: [],
|
|
10958
|
+
customEvents: []
|
|
10959
|
+
});
|
|
10960
|
+
// 缓存优化
|
|
10961
|
+
__publicField(this, "cachedProfile", null);
|
|
10962
|
+
__publicField(this, "cacheTimestamp", 0);
|
|
10963
|
+
__publicField(this, "cacheTTL", 5e3);
|
|
10964
|
+
// 5秒缓存
|
|
10965
|
+
// 异步处理队列
|
|
10966
|
+
__publicField(this, "eventQueue", []);
|
|
10967
|
+
__publicField(this, "isProcessing", false);
|
|
10968
|
+
__publicField(this, "maxQueueSize", 100);
|
|
10969
|
+
// 性能监控
|
|
10970
|
+
__publicField(this, "performanceMetrics", {
|
|
10971
|
+
addEventTimes: [],
|
|
10972
|
+
profileGenerationTimes: []
|
|
10973
|
+
});
|
|
10974
|
+
// 性能模式状态
|
|
10975
|
+
__publicField(this, "performanceModeActive", false);
|
|
10976
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
10977
|
+
this.sessionId = sessionId;
|
|
10978
|
+
this.deviceId = deviceId;
|
|
10979
|
+
this.userId = userId;
|
|
10980
|
+
this.startTime = Date.now();
|
|
10981
|
+
this.maxEvents = options.maxEvents ?? 1e4;
|
|
10982
|
+
this.sampleRate = options.sampleRate ?? 1;
|
|
10983
|
+
this.enableSampling = options.enableSampling ?? true;
|
|
10984
|
+
this.performanceMode = {
|
|
10985
|
+
mode: ((_a = options.performanceMode) == null ? void 0 : _a.mode) ?? "balanced",
|
|
10986
|
+
asyncProcessing: ((_b = options.performanceMode) == null ? void 0 : _b.asyncProcessing) ?? true,
|
|
10987
|
+
useWebWorker: ((_c = options.performanceMode) == null ? void 0 : _c.useWebWorker) ?? false,
|
|
10988
|
+
eventSampleRate: ((_d = options.performanceMode) == null ? void 0 : _d.eventSampleRate) ?? this.sampleRate,
|
|
10989
|
+
maxEvents: ((_e = options.performanceMode) == null ? void 0 : _e.maxEvents) ?? this.maxEvents,
|
|
10990
|
+
enablePerformanceMonitoring: ((_f = options.performanceMode) == null ? void 0 : _f.enablePerformanceMonitoring) ?? true
|
|
10991
|
+
};
|
|
10992
|
+
this.performanceBudget = {
|
|
10993
|
+
addEventMaxTime: ((_g = options.performanceBudget) == null ? void 0 : _g.addEventMaxTime) ?? 1,
|
|
10994
|
+
profileGenerationMaxTime: ((_h = options.performanceBudget) == null ? void 0 : _h.profileGenerationMaxTime) ?? 16
|
|
10995
|
+
};
|
|
10996
|
+
}
|
|
10997
|
+
/**
|
|
10998
|
+
* 添加事件(异步版本,推荐使用)
|
|
10999
|
+
* 快速入队,不阻塞主线程
|
|
11000
|
+
*/
|
|
11001
|
+
addEventAsync(event) {
|
|
11002
|
+
if (!this.performanceMode.asyncProcessing) {
|
|
11003
|
+
this.addEventInternal(event);
|
|
11004
|
+
return;
|
|
11005
|
+
}
|
|
11006
|
+
if (this.eventQueue.length >= this.maxQueueSize) {
|
|
11007
|
+
this.eventQueue.shift();
|
|
11008
|
+
}
|
|
11009
|
+
this.eventQueue.push(event);
|
|
11010
|
+
if (!this.isProcessing) {
|
|
11011
|
+
this.processQueueAsync();
|
|
11012
|
+
}
|
|
11013
|
+
}
|
|
11014
|
+
/**
|
|
11015
|
+
* 添加事件(同步版本)
|
|
11016
|
+
* 注意:如果事件很多,可能影响性能
|
|
11017
|
+
*/
|
|
11018
|
+
addEvent(event) {
|
|
11019
|
+
const start = performance.now();
|
|
11020
|
+
try {
|
|
11021
|
+
if (this.shouldSkipForPerformance(event)) {
|
|
11022
|
+
return;
|
|
11023
|
+
}
|
|
11024
|
+
this.addEventInternal(event);
|
|
11025
|
+
} finally {
|
|
11026
|
+
const duration = performance.now() - start;
|
|
11027
|
+
this.recordPerformance("addEvent", duration);
|
|
11028
|
+
if (duration > this.performanceBudget.addEventMaxTime) {
|
|
11029
|
+
this.enablePerformanceMode();
|
|
11030
|
+
}
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
11033
|
+
/**
|
|
11034
|
+
* 内部添加事件逻辑
|
|
11035
|
+
*/
|
|
11036
|
+
addEventInternal(event) {
|
|
11037
|
+
if (this.events.length >= this.maxEvents) {
|
|
11038
|
+
this.cleanupOldEvents();
|
|
11039
|
+
}
|
|
11040
|
+
if (this.enableSampling && Math.random() > this.sampleRate) {
|
|
11041
|
+
if (event.type !== "error" && event.type !== "custom") {
|
|
11042
|
+
return;
|
|
11043
|
+
}
|
|
11044
|
+
}
|
|
11045
|
+
const lightweightEvent = {
|
|
11046
|
+
type: event.type,
|
|
11047
|
+
name: event.name,
|
|
11048
|
+
timestamp: event.timestamp,
|
|
11049
|
+
url: event.url,
|
|
11050
|
+
title: event.title,
|
|
11051
|
+
sessionId: this.sessionId,
|
|
11052
|
+
deviceId: this.deviceId,
|
|
11053
|
+
userId: this.userId || event.userId,
|
|
11054
|
+
properties: this.sanitizeProperties(event.properties),
|
|
11055
|
+
// 如果是自定义事件,需要保留 eventName
|
|
11056
|
+
...event.type === "custom" && "eventName" in event ? { eventName: event.eventName } : {}
|
|
11057
|
+
};
|
|
11058
|
+
const index = this.events.length;
|
|
11059
|
+
this.events.push(lightweightEvent);
|
|
11060
|
+
this.updateIndexes(event.type, index);
|
|
11061
|
+
this.eventTypeCounts.set(
|
|
11062
|
+
event.type,
|
|
11063
|
+
(this.eventTypeCounts.get(event.type) || 0) + 1
|
|
11064
|
+
);
|
|
11065
|
+
if (event.timestamp < this.lastEventTimestamp) {
|
|
11066
|
+
this.isSorted = false;
|
|
11067
|
+
}
|
|
11068
|
+
this.lastEventTimestamp = event.timestamp;
|
|
11069
|
+
this.cachedProfile = null;
|
|
11070
|
+
}
|
|
11071
|
+
/**
|
|
11072
|
+
* 异步处理队列
|
|
11073
|
+
*/
|
|
11074
|
+
processQueueAsync() {
|
|
11075
|
+
if (this.eventQueue.length === 0) {
|
|
11076
|
+
this.isProcessing = false;
|
|
11077
|
+
return;
|
|
11078
|
+
}
|
|
11079
|
+
this.isProcessing = true;
|
|
11080
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
11081
|
+
requestIdleCallback(
|
|
11082
|
+
() => {
|
|
11083
|
+
this.processBatch();
|
|
11084
|
+
this.processQueueAsync();
|
|
11085
|
+
},
|
|
11086
|
+
{ timeout: 100 }
|
|
11087
|
+
);
|
|
11088
|
+
} else {
|
|
11089
|
+
setTimeout(() => {
|
|
11090
|
+
this.processBatch();
|
|
11091
|
+
this.processQueueAsync();
|
|
11092
|
+
}, 0);
|
|
11093
|
+
}
|
|
11094
|
+
}
|
|
11095
|
+
/**
|
|
11096
|
+
* 处理一批事件
|
|
11097
|
+
*/
|
|
11098
|
+
processBatch() {
|
|
11099
|
+
const batchSize = 10;
|
|
11100
|
+
const batch = this.eventQueue.splice(0, batchSize);
|
|
11101
|
+
batch.forEach((event) => {
|
|
11102
|
+
this.addEventInternal(event);
|
|
11103
|
+
});
|
|
11104
|
+
}
|
|
11105
|
+
/**
|
|
11106
|
+
* 判断是否应该跳过事件(性能模式)
|
|
11107
|
+
*/
|
|
11108
|
+
shouldSkipForPerformance(event) {
|
|
11109
|
+
if (!this.performanceModeActive) {
|
|
11110
|
+
return false;
|
|
11111
|
+
}
|
|
11112
|
+
if (event.type === "error" || event.type === "custom") {
|
|
11113
|
+
return false;
|
|
11114
|
+
}
|
|
11115
|
+
return Math.random() > 0.5;
|
|
11116
|
+
}
|
|
11117
|
+
/**
|
|
11118
|
+
* 启用性能模式
|
|
11119
|
+
*/
|
|
11120
|
+
enablePerformanceMode() {
|
|
11121
|
+
if (!this.performanceModeActive) {
|
|
11122
|
+
console.warn("UserActionSequence: Entering performance mode due to slow operations");
|
|
11123
|
+
this.performanceModeActive = true;
|
|
11124
|
+
this.sampleRate = Math.max(0.1, this.sampleRate * 0.5);
|
|
11125
|
+
this.maxEvents = Math.floor(this.maxEvents * 0.5);
|
|
11126
|
+
this.cleanupOldEvents();
|
|
11127
|
+
}
|
|
11128
|
+
}
|
|
11129
|
+
/**
|
|
11130
|
+
* 清理属性,限制大小
|
|
11131
|
+
*/
|
|
11132
|
+
sanitizeProperties(properties) {
|
|
11133
|
+
if (!properties) return void 0;
|
|
11134
|
+
const sanitized = {};
|
|
11135
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
11136
|
+
const valueStr = JSON.stringify(value);
|
|
11137
|
+
if (valueStr.length > 1e3) {
|
|
11138
|
+
sanitized[key] = valueStr.substring(0, 1e3) + "...";
|
|
11139
|
+
} else {
|
|
11140
|
+
sanitized[key] = value;
|
|
11141
|
+
}
|
|
11142
|
+
}
|
|
11143
|
+
return Object.keys(sanitized).length > 0 ? sanitized : void 0;
|
|
11144
|
+
}
|
|
11145
|
+
/**
|
|
11146
|
+
* 更新索引
|
|
11147
|
+
*/
|
|
11148
|
+
updateIndexes(eventType, index) {
|
|
11149
|
+
switch (eventType) {
|
|
11150
|
+
case "pageview":
|
|
11151
|
+
this.eventIndexes.pageViews.push(index);
|
|
11152
|
+
break;
|
|
11153
|
+
case "click":
|
|
11154
|
+
this.eventIndexes.clicks.push(index);
|
|
11155
|
+
break;
|
|
11156
|
+
case "input":
|
|
11157
|
+
this.eventIndexes.inputs.push(index);
|
|
11158
|
+
break;
|
|
11159
|
+
case "error":
|
|
11160
|
+
this.eventIndexes.errors.push(index);
|
|
11161
|
+
break;
|
|
11162
|
+
case "custom":
|
|
11163
|
+
this.eventIndexes.customEvents.push(index);
|
|
11164
|
+
break;
|
|
11165
|
+
}
|
|
11166
|
+
}
|
|
11167
|
+
/**
|
|
11168
|
+
* 清理旧事件
|
|
11169
|
+
*/
|
|
11170
|
+
cleanupOldEvents() {
|
|
11171
|
+
const keepCount = Math.floor(this.maxEvents / 2);
|
|
11172
|
+
const removedCount = this.events.length - keepCount;
|
|
11173
|
+
this.events = this.events.slice(-keepCount);
|
|
11174
|
+
this.rebuildIndexes();
|
|
11175
|
+
this.recalculateCounts();
|
|
11176
|
+
if (this.performanceMode.enablePerformanceMonitoring) {
|
|
11177
|
+
console.warn(
|
|
11178
|
+
`UserActionSequence: Cleaned up ${removedCount} old events`
|
|
11179
|
+
);
|
|
11180
|
+
}
|
|
11181
|
+
}
|
|
11182
|
+
/**
|
|
11183
|
+
* 重建索引
|
|
11184
|
+
*/
|
|
11185
|
+
rebuildIndexes() {
|
|
11186
|
+
this.eventIndexes = {
|
|
11187
|
+
pageViews: [],
|
|
11188
|
+
clicks: [],
|
|
11189
|
+
inputs: [],
|
|
11190
|
+
errors: [],
|
|
11191
|
+
customEvents: []
|
|
11192
|
+
};
|
|
11193
|
+
this.events.forEach((event, index) => {
|
|
11194
|
+
this.updateIndexes(event.type, index);
|
|
11195
|
+
});
|
|
11196
|
+
}
|
|
11197
|
+
/**
|
|
11198
|
+
* 重新计算统计
|
|
11199
|
+
*/
|
|
11200
|
+
recalculateCounts() {
|
|
11201
|
+
this.eventTypeCounts.clear();
|
|
11202
|
+
this.events.forEach((event) => {
|
|
11203
|
+
this.eventTypeCounts.set(
|
|
11204
|
+
event.type,
|
|
11205
|
+
(this.eventTypeCounts.get(event.type) || 0) + 1
|
|
11206
|
+
);
|
|
11207
|
+
});
|
|
11208
|
+
}
|
|
11209
|
+
/**
|
|
11210
|
+
* 获取事件序列(已排序)
|
|
11211
|
+
*/
|
|
11212
|
+
getSequence() {
|
|
11213
|
+
if (!this.isSorted) {
|
|
11214
|
+
this.events.sort((a, b) => a.timestamp - b.timestamp);
|
|
11215
|
+
this.isSorted = true;
|
|
11216
|
+
this.rebuildIndexes();
|
|
11217
|
+
}
|
|
11218
|
+
return this.events;
|
|
11219
|
+
}
|
|
11220
|
+
/**
|
|
11221
|
+
* 获取会话时长
|
|
11222
|
+
*/
|
|
11223
|
+
getDuration() {
|
|
11224
|
+
if (this.events.length === 0) return 0;
|
|
11225
|
+
const sequence = this.getSequence();
|
|
11226
|
+
const lastEvent = sequence[sequence.length - 1];
|
|
11227
|
+
return lastEvent.timestamp - this.startTime;
|
|
11228
|
+
}
|
|
11229
|
+
/**
|
|
11230
|
+
* 使用索引快速获取特定类型的事件
|
|
11231
|
+
*/
|
|
11232
|
+
getEventsByType(type) {
|
|
11233
|
+
const indexes = this.getIndexesByType(type);
|
|
11234
|
+
const sequence = this.getSequence();
|
|
11235
|
+
return indexes.filter((index) => index >= 0 && index < sequence.length).map((index) => sequence[index]).filter(Boolean);
|
|
11236
|
+
}
|
|
11237
|
+
/**
|
|
11238
|
+
* 根据类型获取索引
|
|
11239
|
+
*/
|
|
11240
|
+
getIndexesByType(type) {
|
|
11241
|
+
switch (type) {
|
|
11242
|
+
case "pageview":
|
|
11243
|
+
return this.eventIndexes.pageViews;
|
|
11244
|
+
case "click":
|
|
11245
|
+
return this.eventIndexes.clicks;
|
|
11246
|
+
case "input":
|
|
11247
|
+
return this.eventIndexes.inputs;
|
|
11248
|
+
case "error":
|
|
11249
|
+
return this.eventIndexes.errors;
|
|
11250
|
+
case "custom":
|
|
11251
|
+
return this.eventIndexes.customEvents;
|
|
11252
|
+
default:
|
|
11253
|
+
return [];
|
|
11254
|
+
}
|
|
11255
|
+
}
|
|
11256
|
+
/**
|
|
11257
|
+
* 生成操作画像(同步版本)
|
|
11258
|
+
*/
|
|
11259
|
+
generateProfile() {
|
|
11260
|
+
const now = Date.now();
|
|
11261
|
+
if (this.cachedProfile && now - this.cacheTimestamp < this.cacheTTL) {
|
|
11262
|
+
return this.cachedProfile;
|
|
11263
|
+
}
|
|
11264
|
+
const sequence = this.getSequence();
|
|
11265
|
+
const filterValidIndexes = (indexes) => {
|
|
11266
|
+
return indexes.filter((index) => index >= 0 && index < sequence.length).map((index) => sequence[index]).filter(Boolean);
|
|
11267
|
+
};
|
|
11268
|
+
const profile = {
|
|
11269
|
+
sessionId: this.sessionId,
|
|
11270
|
+
userId: this.userId,
|
|
11271
|
+
deviceId: this.deviceId,
|
|
11272
|
+
startTime: this.startTime,
|
|
11273
|
+
duration: this.getDuration(),
|
|
11274
|
+
totalEvents: sequence.length,
|
|
11275
|
+
eventTypes: Object.fromEntries(this.eventTypeCounts),
|
|
11276
|
+
pageViews: filterValidIndexes(this.eventIndexes.pageViews),
|
|
11277
|
+
clicks: filterValidIndexes(this.eventIndexes.clicks),
|
|
11278
|
+
inputs: filterValidIndexes(this.eventIndexes.inputs),
|
|
11279
|
+
errors: filterValidIndexes(this.eventIndexes.errors),
|
|
11280
|
+
customEvents: filterValidIndexes(this.eventIndexes.customEvents),
|
|
11281
|
+
userJourney: this.buildUserJourneyOptimized(sequence),
|
|
11282
|
+
behaviorMetrics: this.calculateBehaviorMetricsOptimized(sequence)
|
|
11283
|
+
};
|
|
11284
|
+
this.cachedProfile = profile;
|
|
11285
|
+
this.cacheTimestamp = now;
|
|
11286
|
+
return profile;
|
|
11287
|
+
}
|
|
11288
|
+
/**
|
|
11289
|
+
* 生成操作画像(异步版本)
|
|
11290
|
+
*/
|
|
11291
|
+
generateProfileAsync() {
|
|
11292
|
+
return new Promise((resolve) => {
|
|
11293
|
+
if (typeof window !== "undefined" && "requestIdleCallback" in window) {
|
|
11294
|
+
requestIdleCallback(
|
|
11295
|
+
() => {
|
|
11296
|
+
const start = performance.now();
|
|
11297
|
+
const profile = this.generateProfile();
|
|
11298
|
+
const duration = performance.now() - start;
|
|
11299
|
+
this.recordPerformance("profileGeneration", duration);
|
|
11300
|
+
resolve(profile);
|
|
11301
|
+
},
|
|
11302
|
+
{ timeout: 1e3 }
|
|
11303
|
+
);
|
|
11304
|
+
} else {
|
|
11305
|
+
setTimeout(() => {
|
|
11306
|
+
const start = performance.now();
|
|
11307
|
+
const profile = this.generateProfile();
|
|
11308
|
+
const duration = performance.now() - start;
|
|
11309
|
+
this.recordPerformance("profileGeneration", duration);
|
|
11310
|
+
resolve(profile);
|
|
11311
|
+
}, 0);
|
|
11312
|
+
}
|
|
11313
|
+
});
|
|
11314
|
+
}
|
|
11315
|
+
/**
|
|
11316
|
+
* 生成轻量级画像(不包含完整事件数组)
|
|
11317
|
+
*/
|
|
11318
|
+
generateLightweightProfile() {
|
|
11319
|
+
const sequence = this.getSequence();
|
|
11320
|
+
return {
|
|
11321
|
+
sessionId: this.sessionId,
|
|
11322
|
+
userId: this.userId,
|
|
11323
|
+
deviceId: this.deviceId,
|
|
11324
|
+
startTime: this.startTime,
|
|
11325
|
+
duration: this.getDuration(),
|
|
11326
|
+
totalEvents: sequence.length,
|
|
11327
|
+
eventTypes: Object.fromEntries(this.eventTypeCounts),
|
|
11328
|
+
behaviorMetrics: this.calculateBehaviorMetricsOptimized(sequence)
|
|
11329
|
+
};
|
|
11330
|
+
}
|
|
11331
|
+
/**
|
|
11332
|
+
* 优化的用户旅程构建
|
|
11333
|
+
*/
|
|
11334
|
+
buildUserJourneyOptimized(events) {
|
|
11335
|
+
const journey = [];
|
|
11336
|
+
let prevTimestamp = this.startTime;
|
|
11337
|
+
for (let i = 0; i < events.length; i++) {
|
|
11338
|
+
const event = events[i];
|
|
11339
|
+
journey.push({
|
|
11340
|
+
step: i + 1,
|
|
11341
|
+
timestamp: event.timestamp,
|
|
11342
|
+
type: event.type,
|
|
11343
|
+
url: event.url,
|
|
11344
|
+
title: event.title,
|
|
11345
|
+
action: this.extractAction(event),
|
|
11346
|
+
duration: event.timestamp - prevTimestamp
|
|
11347
|
+
});
|
|
11348
|
+
prevTimestamp = event.timestamp;
|
|
11349
|
+
}
|
|
11350
|
+
return journey;
|
|
11351
|
+
}
|
|
11352
|
+
/**
|
|
11353
|
+
* 提取操作描述
|
|
11354
|
+
*/
|
|
11355
|
+
extractAction(event) {
|
|
11356
|
+
var _a, _b;
|
|
11357
|
+
switch (event.type) {
|
|
11358
|
+
case "pageview":
|
|
11359
|
+
return `访问页面: ${event.title || event.url}`;
|
|
11360
|
+
case "click":
|
|
11361
|
+
const clickEvent = event;
|
|
11362
|
+
return `点击: ${((_a = clickEvent.elementInfo) == null ? void 0 : _a.text) || clickEvent.elementPath || "未知元素"}`;
|
|
11363
|
+
case "input":
|
|
11364
|
+
const inputEvent = event;
|
|
11365
|
+
return `输入: ${((_b = inputEvent.elementInfo) == null ? void 0 : _b.name) || inputEvent.elementPath || "未知字段"}`;
|
|
11366
|
+
case "error":
|
|
11367
|
+
const errorEvent = event;
|
|
11368
|
+
return `错误: ${errorEvent.message}`;
|
|
11369
|
+
case "custom":
|
|
11370
|
+
const customEvent = event;
|
|
11371
|
+
return `自定义事件: ${customEvent.eventName}`;
|
|
11372
|
+
default:
|
|
11373
|
+
return `${event.type} 事件`;
|
|
11374
|
+
}
|
|
11375
|
+
}
|
|
11376
|
+
/**
|
|
11377
|
+
* 优化的行为指标计算
|
|
11378
|
+
*/
|
|
11379
|
+
calculateBehaviorMetricsOptimized(events) {
|
|
11380
|
+
const pageViewIndexes = this.eventIndexes.pageViews.filter(
|
|
11381
|
+
(i) => i >= 0 && i < events.length
|
|
11382
|
+
);
|
|
11383
|
+
const pageViews = pageViewIndexes.map(
|
|
11384
|
+
(i) => events[i]
|
|
11385
|
+
);
|
|
11386
|
+
let totalDuration = 0;
|
|
11387
|
+
const uniquePages = /* @__PURE__ */ new Set();
|
|
11388
|
+
for (const pv of pageViews) {
|
|
11389
|
+
if (pv.duration) {
|
|
11390
|
+
totalDuration += pv.duration;
|
|
11391
|
+
}
|
|
11392
|
+
uniquePages.add(pv.url);
|
|
11393
|
+
}
|
|
11394
|
+
return {
|
|
11395
|
+
pv: pageViewIndexes.length,
|
|
11396
|
+
avgPageDuration: pageViewIndexes.length > 0 ? totalDuration / pageViewIndexes.length : 0,
|
|
11397
|
+
clickCount: this.eventIndexes.clicks.length,
|
|
11398
|
+
inputCount: this.eventIndexes.inputs.length,
|
|
11399
|
+
errorCount: this.eventIndexes.errors.length,
|
|
11400
|
+
uniquePages: uniquePages.size
|
|
11401
|
+
};
|
|
11402
|
+
}
|
|
11403
|
+
/**
|
|
11404
|
+
* 记录性能指标
|
|
11405
|
+
*/
|
|
11406
|
+
recordPerformance(operation, duration) {
|
|
11407
|
+
if (!this.performanceMode.enablePerformanceMonitoring) {
|
|
11408
|
+
return;
|
|
11409
|
+
}
|
|
11410
|
+
const metrics = this.performanceMetrics[`${operation}Times`];
|
|
11411
|
+
if (metrics) {
|
|
11412
|
+
metrics.push(duration);
|
|
11413
|
+
if (metrics.length > 100) {
|
|
11414
|
+
metrics.shift();
|
|
11415
|
+
}
|
|
11416
|
+
}
|
|
11417
|
+
}
|
|
11418
|
+
/**
|
|
11419
|
+
* 获取性能统计
|
|
11420
|
+
*/
|
|
11421
|
+
getPerformanceStats() {
|
|
11422
|
+
const addEventTimes = this.performanceMetrics.addEventTimes;
|
|
11423
|
+
const profileTimes = this.performanceMetrics.profileGenerationTimes;
|
|
11424
|
+
return {
|
|
11425
|
+
avgAddEventTime: addEventTimes.length > 0 ? addEventTimes.reduce((a, b) => a + b, 0) / addEventTimes.length : 0,
|
|
11426
|
+
avgProfileGenerationTime: profileTimes.length > 0 ? profileTimes.reduce((a, b) => a + b, 0) / profileTimes.length : 0,
|
|
11427
|
+
p95AddEventTime: this.percentile(addEventTimes, 95),
|
|
11428
|
+
p95ProfileGenerationTime: this.percentile(profileTimes, 95)
|
|
11429
|
+
};
|
|
11430
|
+
}
|
|
11431
|
+
/**
|
|
11432
|
+
* 计算百分位数
|
|
11433
|
+
*/
|
|
11434
|
+
percentile(arr, p) {
|
|
11435
|
+
if (arr.length === 0) return 0;
|
|
11436
|
+
const sorted = [...arr].sort((a, b) => a - b);
|
|
11437
|
+
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
11438
|
+
return sorted[Math.max(0, index)] || 0;
|
|
11439
|
+
}
|
|
11440
|
+
/**
|
|
11441
|
+
* 重置序列
|
|
11442
|
+
*/
|
|
11443
|
+
reset() {
|
|
11444
|
+
this.events = [];
|
|
11445
|
+
this.eventQueue = [];
|
|
11446
|
+
this.eventTypeCounts.clear();
|
|
11447
|
+
this.eventIndexes = {
|
|
11448
|
+
pageViews: [],
|
|
11449
|
+
clicks: [],
|
|
11450
|
+
inputs: [],
|
|
11451
|
+
errors: [],
|
|
11452
|
+
customEvents: []
|
|
11453
|
+
};
|
|
11454
|
+
this.cachedProfile = null;
|
|
11455
|
+
this.isSorted = true;
|
|
11456
|
+
this.lastEventTimestamp = 0;
|
|
11457
|
+
this.startTime = Date.now();
|
|
11458
|
+
this.performanceModeActive = false;
|
|
11459
|
+
}
|
|
11460
|
+
}
|
|
8433
11461
|
export {
|
|
11462
|
+
PerformanceMonitor,
|
|
11463
|
+
UserActionSequence,
|
|
8434
11464
|
createNetworkInterceptorPlugin,
|
|
8435
11465
|
Tracker as default,
|
|
8436
11466
|
eventBus,
|
|
11467
|
+
extractNaiveUIComponentInfo,
|
|
11468
|
+
extractVueComponentInfo,
|
|
11469
|
+
filterSensitiveData,
|
|
11470
|
+
getComponentAttrs,
|
|
11471
|
+
getComponentName,
|
|
11472
|
+
getComponentProps,
|
|
11473
|
+
getComponentValue,
|
|
11474
|
+
getDeviceId,
|
|
11475
|
+
getNaiveUIComponentTypes,
|
|
11476
|
+
getSessionId,
|
|
11477
|
+
getVueComponentInstance,
|
|
11478
|
+
identifyNaiveUIComponent,
|
|
8437
11479
|
indexedDBStorage,
|
|
11480
|
+
isNaiveUIComponent,
|
|
11481
|
+
isNaiveUIComponentType,
|
|
11482
|
+
isVueComponent,
|
|
8438
11483
|
localStorage$1 as localStorage,
|
|
8439
11484
|
memoryStorage,
|
|
8440
11485
|
networkInterceptorPlugin,
|
|
8441
|
-
sessionStorage$1 as sessionStorage
|
|
11486
|
+
sessionStorage$1 as sessionStorage,
|
|
11487
|
+
useTracker,
|
|
11488
|
+
vuePlugin
|
|
8442
11489
|
};
|
|
8443
11490
|
//# sourceMappingURL=index.es.js.map
|