onerway-analytics 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundle/index.js +1361 -0
- package/dist/bundle/index.js.map +7 -0
- package/dist/core/analytics.d.ts +45 -0
- package/dist/core/analytics.d.ts.map +1 -0
- package/dist/core/analytics.js +369 -0
- package/dist/core/analytics.test.d.ts +2 -0
- package/dist/core/analytics.test.d.ts.map +1 -0
- package/dist/core/analytics.test.js +169 -0
- package/dist/core/auto-track.d.ts +6 -0
- package/dist/core/auto-track.d.ts.map +1 -0
- package/dist/core/auto-track.js +126 -0
- package/dist/core/context-collector.d.ts +4 -0
- package/dist/core/context-collector.d.ts.map +1 -0
- package/dist/core/context-collector.js +42 -0
- package/dist/core/error-monitor.d.ts +5 -0
- package/dist/core/error-monitor.d.ts.map +1 -0
- package/dist/core/error-monitor.js +61 -0
- package/dist/core/ga-adapter.d.ts +12 -0
- package/dist/core/ga-adapter.d.ts.map +1 -0
- package/dist/core/ga-adapter.js +35 -0
- package/dist/core/ga-loader.d.ts +10 -0
- package/dist/core/ga-loader.d.ts.map +1 -0
- package/dist/core/ga-loader.js +30 -0
- package/dist/core/ga-manager.d.ts +24 -0
- package/dist/core/ga-manager.d.ts.map +1 -0
- package/dist/core/ga-manager.js +64 -0
- package/dist/core/performance-monitor.d.ts +5 -0
- package/dist/core/performance-monitor.d.ts.map +1 -0
- package/dist/core/performance-monitor.js +43 -0
- package/dist/core/sender.d.ts +10 -0
- package/dist/core/sender.d.ts.map +1 -0
- package/dist/core/sender.js +65 -0
- package/dist/core/session-manager.d.ts +13 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/core/session-manager.js +54 -0
- package/dist/core/storage.d.ts +9 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/storage.js +48 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/types/index.d.ts +162 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +28 -0
- package/dist/utils/index.d.ts +30 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +216 -0
- package/package.json +47 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { createAnalytics } from '../core/analytics';
|
|
3
|
+
import { StorageQueue } from '../core/storage';
|
|
4
|
+
function createMockXHR() {
|
|
5
|
+
const calls = [];
|
|
6
|
+
globalThis.XMLHttpRequest = class {
|
|
7
|
+
constructor() {
|
|
8
|
+
this._url = '';
|
|
9
|
+
this._body = '';
|
|
10
|
+
this._headers = {};
|
|
11
|
+
this.status = 0;
|
|
12
|
+
this.statusText = '';
|
|
13
|
+
this.readyState = 0;
|
|
14
|
+
this.onload = null;
|
|
15
|
+
this.onerror = null;
|
|
16
|
+
this.ontimeout = null;
|
|
17
|
+
this.timeout = 0;
|
|
18
|
+
}
|
|
19
|
+
open(_method, url) {
|
|
20
|
+
this._url = url;
|
|
21
|
+
}
|
|
22
|
+
setRequestHeader(key, value) {
|
|
23
|
+
this._headers[key] = value;
|
|
24
|
+
}
|
|
25
|
+
send(body) {
|
|
26
|
+
this._body = body || '';
|
|
27
|
+
calls.push({ url: this._url, body: this._body, headers: { ...this._headers } });
|
|
28
|
+
setTimeout(() => {
|
|
29
|
+
this.status = 200;
|
|
30
|
+
this.statusText = 'OK';
|
|
31
|
+
this.readyState = 4;
|
|
32
|
+
if (this.onload)
|
|
33
|
+
this.onload({});
|
|
34
|
+
}, 0);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
return calls;
|
|
38
|
+
}
|
|
39
|
+
describe('Analytics SDK', () => {
|
|
40
|
+
let analytics;
|
|
41
|
+
let xhrCalls;
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
localStorage.clear();
|
|
44
|
+
sessionStorage.clear();
|
|
45
|
+
xhrCalls = createMockXHR();
|
|
46
|
+
analytics = createAnalytics({
|
|
47
|
+
appKey: 'test-app-id',
|
|
48
|
+
serverUrl: 'https://example.com/track',
|
|
49
|
+
apiKey: 'test-api-key',
|
|
50
|
+
debug: false,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
analytics.reset();
|
|
55
|
+
jest.restoreAllMocks();
|
|
56
|
+
});
|
|
57
|
+
describe('track', () => {
|
|
58
|
+
it('should send events on flush', async () => {
|
|
59
|
+
analytics.track('test_event', { custom_prop: 'value' });
|
|
60
|
+
analytics.flush();
|
|
61
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
62
|
+
expect(xhrCalls.length).toBeGreaterThanOrEqual(1);
|
|
63
|
+
const body = JSON.parse(xhrCalls[0].body);
|
|
64
|
+
const pageViewEvent = body.events.find((e) => e.event_name === 'page_view');
|
|
65
|
+
const testEvent = body.events.find((e) => e.event_name === 'test_event');
|
|
66
|
+
expect(pageViewEvent).toBeDefined();
|
|
67
|
+
expect(testEvent).toBeDefined();
|
|
68
|
+
expect(testEvent.event_properties.custom_prop).toBe('value');
|
|
69
|
+
});
|
|
70
|
+
it('should ignore invalid event name', async () => {
|
|
71
|
+
analytics.track('');
|
|
72
|
+
analytics.track(123);
|
|
73
|
+
analytics.flush();
|
|
74
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
75
|
+
if (xhrCalls.length > 0) {
|
|
76
|
+
const body = JSON.parse(xhrCalls[0].body);
|
|
77
|
+
const names = body.events.map((e) => e.event_name);
|
|
78
|
+
expect(names).not.toContain('');
|
|
79
|
+
expect(names).toContain('page_view');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
describe('identify', () => {
|
|
84
|
+
it('should set user profile', () => {
|
|
85
|
+
analytics.identify('user-123', { name: 'Test User' });
|
|
86
|
+
const profile = analytics.getUserProfile();
|
|
87
|
+
expect(profile.userId).toBe('user-123');
|
|
88
|
+
expect(profile.attributes).toEqual({ name: 'Test User' });
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
describe('reset', () => {
|
|
92
|
+
it('should clear user profile and queue', () => {
|
|
93
|
+
analytics.identify('user-123', { name: 'Test User' });
|
|
94
|
+
analytics.track('some_event');
|
|
95
|
+
analytics.reset();
|
|
96
|
+
const profile = analytics.getUserProfile();
|
|
97
|
+
expect(profile.userId).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('configuration', () => {
|
|
101
|
+
it('should support appId backward compatibility', () => {
|
|
102
|
+
const analytics2 = createAnalytics({
|
|
103
|
+
appId: 'backward-compat-app',
|
|
104
|
+
serverUrl: 'https://example.com/track',
|
|
105
|
+
apiKey: 'test-api-key',
|
|
106
|
+
debug: false,
|
|
107
|
+
});
|
|
108
|
+
expect(analytics2.getClientId()).toBeDefined();
|
|
109
|
+
analytics2.reset();
|
|
110
|
+
});
|
|
111
|
+
it('should generate client_id on creation', () => {
|
|
112
|
+
const cid = analytics.getClientId();
|
|
113
|
+
expect(cid).toBeDefined();
|
|
114
|
+
expect(typeof cid).toBe('string');
|
|
115
|
+
expect(cid.length).toBeGreaterThan(0);
|
|
116
|
+
});
|
|
117
|
+
it('should generate session_id on creation', () => {
|
|
118
|
+
const sid = analytics.getSessionId();
|
|
119
|
+
expect(sid).toBeDefined();
|
|
120
|
+
expect(typeof sid).toBe('string');
|
|
121
|
+
expect(sid.length).toBeGreaterThan(0);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('payload structure', () => {
|
|
125
|
+
it('should assemble correct request payload', async () => {
|
|
126
|
+
analytics.track('test_event', { scene: 'checkout.payment' });
|
|
127
|
+
analytics.flush();
|
|
128
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
129
|
+
expect(xhrCalls.length).toBeGreaterThanOrEqual(1);
|
|
130
|
+
const body = JSON.parse(xhrCalls[0].body);
|
|
131
|
+
expect(body.client_id).toBeDefined();
|
|
132
|
+
expect(body.session_id).toBeDefined();
|
|
133
|
+
expect(body.user_id).toBe('');
|
|
134
|
+
expect(body.user_properties).toBeUndefined();
|
|
135
|
+
expect(body.context).toBeDefined();
|
|
136
|
+
expect(body.context.app_key).toBe('test-app-id');
|
|
137
|
+
expect(body.context.sdk_version).toBeDefined();
|
|
138
|
+
expect(body.marketing).toBeDefined();
|
|
139
|
+
expect(body.events).toBeInstanceOf(Array);
|
|
140
|
+
const testEvent = body.events.find((e) => e.event_name === 'test_event');
|
|
141
|
+
expect(testEvent).toBeDefined();
|
|
142
|
+
expect(testEvent.event_id).toBeDefined();
|
|
143
|
+
expect(testEvent.timestamp).toBeGreaterThan(0);
|
|
144
|
+
expect(testEvent.key_event).toBe(false);
|
|
145
|
+
expect(testEvent.event_properties.scene).toBe('checkout.payment');
|
|
146
|
+
expect(testEvent.batch_ordering_id).toBeDefined();
|
|
147
|
+
expect(testEvent.batch_page_id).toBeDefined();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('StorageQueue', () => {
|
|
151
|
+
it('should persist and recover failed events', () => {
|
|
152
|
+
StorageQueue.clear();
|
|
153
|
+
StorageQueue.enqueue({
|
|
154
|
+
event_id: 'evt-1',
|
|
155
|
+
event_name: 'failed_event',
|
|
156
|
+
batch_event_index: 0,
|
|
157
|
+
batch_ordering_id: 'ord-1',
|
|
158
|
+
batch_page_id: 'page-1',
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
key_event: false,
|
|
161
|
+
event_properties: {},
|
|
162
|
+
});
|
|
163
|
+
const recovered = StorageQueue.recover();
|
|
164
|
+
expect(recovered.length).toBe(1);
|
|
165
|
+
expect(recovered[0].event_name).toBe('failed_event');
|
|
166
|
+
expect(StorageQueue.getQueueSize()).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-track.d.ts","sourceRoot":"","sources":["../../src/core/auto-track.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAyBxE"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { truncateText } from '../utils';
|
|
2
|
+
const TRACK_ATTR_PREFIX = 'data-track-';
|
|
3
|
+
const IGNORE_ATTR = 'data-track-ignore';
|
|
4
|
+
const NAME_ATTR = 'data-track-name';
|
|
5
|
+
const PROPS_ATTR = 'data-track-properties';
|
|
6
|
+
export function initAutoTrack(api, selector) {
|
|
7
|
+
if (typeof document === 'undefined')
|
|
8
|
+
return;
|
|
9
|
+
const handler = (e) => {
|
|
10
|
+
try {
|
|
11
|
+
const target = e.target;
|
|
12
|
+
if (!target)
|
|
13
|
+
return;
|
|
14
|
+
if (shouldIgnoreElement(target))
|
|
15
|
+
return;
|
|
16
|
+
if (selector) {
|
|
17
|
+
const scope = document.querySelector(selector);
|
|
18
|
+
if (scope && !scope.contains(target))
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const eventName = getTrackName(target);
|
|
22
|
+
const properties = collectElementProperties(target);
|
|
23
|
+
api.track(eventName, properties);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Fail silently to avoid breaking user's page
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
document.addEventListener('click', handler, true);
|
|
30
|
+
}
|
|
31
|
+
function shouldIgnoreElement(element) {
|
|
32
|
+
let current = element;
|
|
33
|
+
while (current) {
|
|
34
|
+
if (current.hasAttribute(IGNORE_ATTR))
|
|
35
|
+
return true;
|
|
36
|
+
current = current.parentElement;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
function getTrackName(element) {
|
|
41
|
+
const name = element.getAttribute(NAME_ATTR);
|
|
42
|
+
if (name)
|
|
43
|
+
return name;
|
|
44
|
+
const id = element.id ? `_${element.id}` : '';
|
|
45
|
+
const tag = element.tagName.toLowerCase();
|
|
46
|
+
return `click_${tag}${id}`;
|
|
47
|
+
}
|
|
48
|
+
function collectElementProperties(element) {
|
|
49
|
+
const props = {
|
|
50
|
+
action: 'click',
|
|
51
|
+
target_type: inferTargetType(element),
|
|
52
|
+
target_id: getTargetId(element),
|
|
53
|
+
target_tag: element.tagName.toLowerCase(),
|
|
54
|
+
target_class: getTruncatedClassName(element),
|
|
55
|
+
target_text: truncateText(element.innerText || '', 50),
|
|
56
|
+
};
|
|
57
|
+
if (element.hasAttribute('href') || element.closest('a')) {
|
|
58
|
+
const anchor = element.closest('a');
|
|
59
|
+
if (anchor) {
|
|
60
|
+
props.target_href = anchor.getAttribute('href') || '';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const customProps = collectDataTrackProps(element);
|
|
64
|
+
Object.assign(props, customProps);
|
|
65
|
+
return props;
|
|
66
|
+
}
|
|
67
|
+
function inferTargetType(element) {
|
|
68
|
+
const tag = element.tagName.toLowerCase();
|
|
69
|
+
if (tag === 'a' || tag === 'button')
|
|
70
|
+
return 'button';
|
|
71
|
+
if (tag === 'input') {
|
|
72
|
+
const type = element.type;
|
|
73
|
+
if (type === 'submit' || type === 'button' || type === 'reset')
|
|
74
|
+
return 'button';
|
|
75
|
+
if (type === 'checkbox' || type === 'radio')
|
|
76
|
+
return 'checkbox';
|
|
77
|
+
return 'input';
|
|
78
|
+
}
|
|
79
|
+
if (tag === 'select')
|
|
80
|
+
return 'select';
|
|
81
|
+
if (tag === 'img')
|
|
82
|
+
return 'image';
|
|
83
|
+
if (tag === 'form')
|
|
84
|
+
return 'form';
|
|
85
|
+
if (tag === 'textarea')
|
|
86
|
+
return 'input';
|
|
87
|
+
return 'element';
|
|
88
|
+
}
|
|
89
|
+
function getTargetId(element) {
|
|
90
|
+
if (element.id)
|
|
91
|
+
return element.id;
|
|
92
|
+
const name = element.getAttribute('name');
|
|
93
|
+
if (name)
|
|
94
|
+
return name;
|
|
95
|
+
const dataTestId = element.getAttribute('data-testid') || element.getAttribute('data-test-id');
|
|
96
|
+
if (dataTestId)
|
|
97
|
+
return dataTestId;
|
|
98
|
+
return '';
|
|
99
|
+
}
|
|
100
|
+
function getTruncatedClassName(element) {
|
|
101
|
+
const cls = typeof element.className === 'string' ? element.className : '';
|
|
102
|
+
return truncateText(cls, 100);
|
|
103
|
+
}
|
|
104
|
+
function collectDataTrackProps(element) {
|
|
105
|
+
const props = {};
|
|
106
|
+
const trackName = element.getAttribute(NAME_ATTR);
|
|
107
|
+
if (trackName)
|
|
108
|
+
props.name = trackName;
|
|
109
|
+
const rawProps = element.getAttribute(PROPS_ATTR);
|
|
110
|
+
if (rawProps) {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(rawProps);
|
|
113
|
+
Object.assign(props, parsed);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// Invalid JSON, ignore
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const attr of Array.from(element.attributes)) {
|
|
120
|
+
if (attr.name.startsWith(TRACK_ATTR_PREFIX) && attr.name !== NAME_ATTR && attr.name !== PROPS_ATTR && attr.name !== IGNORE_ATTR) {
|
|
121
|
+
const key = attr.name.replace(TRACK_ATTR_PREFIX, '');
|
|
122
|
+
props[key] = attr.value;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return props;
|
|
126
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { ContextPayload, MarketingPayload } from '../types';
|
|
2
|
+
export declare function collectContext(appKey: string, merchantLanguage: string, clientType: string): ContextPayload;
|
|
3
|
+
export declare function collectMarketing(): MarketingPayload;
|
|
4
|
+
//# sourceMappingURL=context-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context-collector.d.ts","sourceRoot":"","sources":["../../src/core/context-collector.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,gBAAgB,EAGjB,MAAM,UAAU,CAAC;AAiBlB,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,cAAc,CAoB3G;AAED,wBAAgB,gBAAgB,IAAI,gBAAgB,CAsBnD"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { SDK_VERSION, STORAGE_KEYS, } from '../types';
|
|
2
|
+
import { detectBrowser, detectOS, getDeviceType, getScreenSize, getHostname, getReferrer, getTimezone, getLanguage, getCountryFromLanguage, parseUTMParams, safeLocalStorageGet, safeLocalStorageSet, isBrowser, } from '../utils';
|
|
3
|
+
export function collectContext(appKey, merchantLanguage, clientType) {
|
|
4
|
+
const screen = getScreenSize();
|
|
5
|
+
return {
|
|
6
|
+
sdk_version: SDK_VERSION,
|
|
7
|
+
app_key: appKey,
|
|
8
|
+
screen_width: screen.width,
|
|
9
|
+
screen_height: screen.height,
|
|
10
|
+
user_agent: isBrowser() ? navigator.userAgent : '',
|
|
11
|
+
referrer: getReferrer(),
|
|
12
|
+
hostname: getHostname(),
|
|
13
|
+
browser: detectBrowser(),
|
|
14
|
+
operating_system: detectOS(),
|
|
15
|
+
device_type: getDeviceType(),
|
|
16
|
+
country: getCountryFromLanguage(getLanguage()),
|
|
17
|
+
language: getLanguage(),
|
|
18
|
+
merchant_language: merchantLanguage,
|
|
19
|
+
timezone: getTimezone(),
|
|
20
|
+
client_type: clientType,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export function collectMarketing() {
|
|
24
|
+
if (!isBrowser())
|
|
25
|
+
return {};
|
|
26
|
+
const utm = parseUTMParams();
|
|
27
|
+
let landingPage = safeLocalStorageGet(STORAGE_KEYS.LANDING_PAGE);
|
|
28
|
+
if (!landingPage) {
|
|
29
|
+
landingPage = window.location.href;
|
|
30
|
+
safeLocalStorageSet(STORAGE_KEYS.LANDING_PAGE, landingPage);
|
|
31
|
+
}
|
|
32
|
+
let referrerUrl = safeLocalStorageGet(STORAGE_KEYS.REFERRER_URL);
|
|
33
|
+
if (referrerUrl === null && document.referrer) {
|
|
34
|
+
referrerUrl = document.referrer;
|
|
35
|
+
safeLocalStorageSet(STORAGE_KEYS.REFERRER_URL, referrerUrl);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...utm,
|
|
39
|
+
landing_page: landingPage || window.location.href,
|
|
40
|
+
referrer_url: referrerUrl || '',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-monitor.d.ts","sourceRoot":"","sources":["../../src/core/error-monitor.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CACtE;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,IAAI,CA4D3D"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function initErrorMonitor(api) {
|
|
2
|
+
if (typeof window === 'undefined')
|
|
3
|
+
return;
|
|
4
|
+
window.addEventListener('error', (event) => {
|
|
5
|
+
try {
|
|
6
|
+
const { message, filename, lineno, colno, error } = event;
|
|
7
|
+
const isResourceError = !message && event.target !== window;
|
|
8
|
+
if (isResourceError)
|
|
9
|
+
return;
|
|
10
|
+
api.track('error_event', {
|
|
11
|
+
action: 'error',
|
|
12
|
+
target_type: 'system',
|
|
13
|
+
error_type: 'window_error',
|
|
14
|
+
error_message: message || 'Unknown error',
|
|
15
|
+
error_filename: filename || '',
|
|
16
|
+
error_lineno: lineno || 0,
|
|
17
|
+
error_colno: colno || 0,
|
|
18
|
+
error_stack: error instanceof Error ? error.stack : '',
|
|
19
|
+
error_name: error instanceof Error ? error.name : '',
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Fail silently to avoid recursive error reporting
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
27
|
+
try {
|
|
28
|
+
const { reason } = event;
|
|
29
|
+
let errorMessage = 'Unhandled Promise Rejection';
|
|
30
|
+
let errorStack = '';
|
|
31
|
+
let errorName = 'PromiseRejection';
|
|
32
|
+
if (reason instanceof Error) {
|
|
33
|
+
errorMessage = reason.message;
|
|
34
|
+
errorStack = reason.stack || '';
|
|
35
|
+
errorName = reason.name;
|
|
36
|
+
}
|
|
37
|
+
else if (typeof reason === 'string') {
|
|
38
|
+
errorMessage = reason;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
try {
|
|
42
|
+
errorMessage = JSON.stringify(reason);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
errorMessage = String(reason);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
api.track('error_event', {
|
|
49
|
+
action: 'error',
|
|
50
|
+
target_type: 'system',
|
|
51
|
+
error_type: 'unhandled_rejection',
|
|
52
|
+
error_message: errorMessage,
|
|
53
|
+
error_stack: errorStack,
|
|
54
|
+
error_name: errorName,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Fail silently
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { MarketingPayload } from '../types';
|
|
2
|
+
export interface GAEventOptions {
|
|
3
|
+
sessionId: string;
|
|
4
|
+
pageUrl: string;
|
|
5
|
+
referrer: string;
|
|
6
|
+
marketing: MarketingPayload;
|
|
7
|
+
userId?: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function trackToGA(eventName: string, properties: Record<string, unknown>, options: GAEventOptions): void;
|
|
10
|
+
export declare function setGAUserId(userId: string | null): void;
|
|
11
|
+
export declare function setGAUserProperties(properties: Record<string, unknown>): void;
|
|
12
|
+
//# sourceMappingURL=ga-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ga-adapter.d.ts","sourceRoot":"","sources":["../../src/core/ga-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE5C,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,wBAAgB,SAAS,CACvB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACnC,OAAO,EAAE,cAAc,GACtB,IAAI,CAqBN;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGvD;AAED,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAG7E"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function trackToGA(eventName, properties, options) {
|
|
2
|
+
if (typeof window === 'undefined' || !window.gtag)
|
|
3
|
+
return;
|
|
4
|
+
const params = {
|
|
5
|
+
session_id: options.sessionId,
|
|
6
|
+
engagement_time_msec: 100,
|
|
7
|
+
page_location: options.pageUrl,
|
|
8
|
+
page_referrer: options.referrer,
|
|
9
|
+
};
|
|
10
|
+
const { marketing, userId } = options;
|
|
11
|
+
if (marketing.utm_source)
|
|
12
|
+
params.utm_source = marketing.utm_source;
|
|
13
|
+
if (marketing.utm_medium)
|
|
14
|
+
params.utm_medium = marketing.utm_medium;
|
|
15
|
+
if (marketing.utm_campaign)
|
|
16
|
+
params.utm_campaign = marketing.utm_campaign;
|
|
17
|
+
if (marketing.utm_term)
|
|
18
|
+
params.utm_term = marketing.utm_term;
|
|
19
|
+
if (marketing.utm_content)
|
|
20
|
+
params.utm_content = marketing.utm_content;
|
|
21
|
+
if (userId)
|
|
22
|
+
params.user_id = userId;
|
|
23
|
+
Object.assign(params, properties);
|
|
24
|
+
window.gtag('event', eventName, params);
|
|
25
|
+
}
|
|
26
|
+
export function setGAUserId(userId) {
|
|
27
|
+
if (typeof window === 'undefined' || !window.gtag)
|
|
28
|
+
return;
|
|
29
|
+
window.gtag('set', { user_id: userId ?? undefined });
|
|
30
|
+
}
|
|
31
|
+
export function setGAUserProperties(properties) {
|
|
32
|
+
if (typeof window === 'undefined' || !window.gtag)
|
|
33
|
+
return;
|
|
34
|
+
window.gtag('set', 'user_properties', properties);
|
|
35
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GAConfig } from '../types';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
gtag: (...args: unknown[]) => void;
|
|
5
|
+
dataLayer: unknown[];
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare function loadGtagScript(measurementId: string): Promise<void>;
|
|
9
|
+
export declare function initGA(config: GAConfig): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=ga-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ga-loader.d.ts","sourceRoot":"","sources":["../../src/core/ga-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;QACnC,SAAS,EAAE,OAAO,EAAE,CAAC;KACtB;CACF;AAED,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAcnE;AAED,wBAAsB,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB5D"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function loadGtagScript(measurementId) {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
|
4
|
+
resolve();
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
const script = document.createElement('script');
|
|
8
|
+
script.async = true;
|
|
9
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
|
|
10
|
+
script.onload = () => resolve();
|
|
11
|
+
script.onerror = () => reject(new Error('Failed to load gtag.js'));
|
|
12
|
+
document.head.appendChild(script);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function initGA(config) {
|
|
16
|
+
if (!config.enabled || !config.measurementId)
|
|
17
|
+
return;
|
|
18
|
+
if (typeof window === 'undefined')
|
|
19
|
+
return;
|
|
20
|
+
await loadGtagScript(config.measurementId);
|
|
21
|
+
window.dataLayer = window.dataLayer || [];
|
|
22
|
+
window.gtag = function gtag(...args) {
|
|
23
|
+
window.dataLayer.push(args);
|
|
24
|
+
};
|
|
25
|
+
window.gtag('js', new Date());
|
|
26
|
+
window.gtag('config', config.measurementId, {
|
|
27
|
+
send_page_view: config.sendPageView ?? false,
|
|
28
|
+
debug_mode: config.debugMode ?? false,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GAConsentMode, GAConsentParams } from '../types';
|
|
2
|
+
declare global {
|
|
3
|
+
interface Window {
|
|
4
|
+
gtag: (...args: unknown[]) => void;
|
|
5
|
+
dataLayer: unknown[];
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare class GAManager {
|
|
9
|
+
private _initialized;
|
|
10
|
+
private _measurementId;
|
|
11
|
+
get ready(): boolean;
|
|
12
|
+
get measurementId(): string | undefined;
|
|
13
|
+
track(eventName: string, properties?: Record<string, unknown>): void;
|
|
14
|
+
setUserId(userId: string | null): void;
|
|
15
|
+
setUserProperties(properties: Record<string, unknown>): void;
|
|
16
|
+
set(keyOrParams: string | Record<string, unknown>, value?: unknown): void;
|
|
17
|
+
config(params?: Record<string, unknown>): void;
|
|
18
|
+
consent(mode: GAConsentMode, params: GAConsentParams): void;
|
|
19
|
+
get(key: string, callback: (value: unknown) => void): void;
|
|
20
|
+
gtag(...args: unknown[]): void;
|
|
21
|
+
_init(measurementId: string): void;
|
|
22
|
+
_destroy(): void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ga-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ga-manager.d.ts","sourceRoot":"","sources":["../../src/core/ga-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EAChB,MAAM,UAAU,CAAC;AAElB,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;QACnC,SAAS,EAAE,OAAO,EAAE,CAAC;KACtB;CACF;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,cAAc,CAAqB;IAE3C,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAEtC;IAED,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAKpE,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAKtC,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAK5D,GAAG,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IASzE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAK9C,OAAO,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAK3D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAK1D,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAK9B,KAAK,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI;IAKlC,QAAQ,IAAI,IAAI;CAIjB"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export class GAManager {
|
|
2
|
+
constructor() {
|
|
3
|
+
this._initialized = false;
|
|
4
|
+
}
|
|
5
|
+
get ready() {
|
|
6
|
+
return this._initialized && typeof window !== 'undefined' && typeof window.gtag === 'function';
|
|
7
|
+
}
|
|
8
|
+
get measurementId() {
|
|
9
|
+
return this._measurementId;
|
|
10
|
+
}
|
|
11
|
+
track(eventName, properties) {
|
|
12
|
+
if (!this.ready)
|
|
13
|
+
return;
|
|
14
|
+
window.gtag('event', eventName, properties ?? {});
|
|
15
|
+
}
|
|
16
|
+
setUserId(userId) {
|
|
17
|
+
if (!this.ready)
|
|
18
|
+
return;
|
|
19
|
+
window.gtag('set', { user_id: userId ?? undefined });
|
|
20
|
+
}
|
|
21
|
+
setUserProperties(properties) {
|
|
22
|
+
if (!this.ready)
|
|
23
|
+
return;
|
|
24
|
+
window.gtag('set', 'user_properties', properties);
|
|
25
|
+
}
|
|
26
|
+
set(keyOrParams, value) {
|
|
27
|
+
if (!this.ready)
|
|
28
|
+
return;
|
|
29
|
+
if (typeof keyOrParams === 'string') {
|
|
30
|
+
window.gtag('set', keyOrParams, value);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
window.gtag('set', keyOrParams);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
config(params) {
|
|
37
|
+
if (!this.ready || !this._measurementId)
|
|
38
|
+
return;
|
|
39
|
+
window.gtag('config', this._measurementId, params ?? {});
|
|
40
|
+
}
|
|
41
|
+
consent(mode, params) {
|
|
42
|
+
if (!this.ready)
|
|
43
|
+
return;
|
|
44
|
+
window.gtag('consent', mode, params);
|
|
45
|
+
}
|
|
46
|
+
get(key, callback) {
|
|
47
|
+
if (!this.ready || !this._measurementId)
|
|
48
|
+
return;
|
|
49
|
+
window.gtag('get', this._measurementId, key, callback);
|
|
50
|
+
}
|
|
51
|
+
gtag(...args) {
|
|
52
|
+
if (typeof window === 'undefined' || !window.gtag)
|
|
53
|
+
return;
|
|
54
|
+
window.gtag(...args);
|
|
55
|
+
}
|
|
56
|
+
_init(measurementId) {
|
|
57
|
+
this._measurementId = measurementId;
|
|
58
|
+
this._initialized = true;
|
|
59
|
+
}
|
|
60
|
+
_destroy() {
|
|
61
|
+
this._initialized = false;
|
|
62
|
+
this._measurementId = undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Analytics } from './analytics';
|
|
2
|
+
import type { PerformanceConfig } from '../types';
|
|
3
|
+
export type PerformanceMonitorAPI = Pick<Analytics, 'track'>;
|
|
4
|
+
export declare function initPerformanceMonitor(api: PerformanceMonitorAPI, config?: PerformanceConfig): void;
|
|
5
|
+
//# sourceMappingURL=performance-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-monitor.d.ts","sourceRoot":"","sources":["../../src/core/performance-monitor.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,MAAM,MAAM,qBAAqB,GAAG,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;AAkC7D,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,qBAAqB,EAC1B,MAAM,GAAE,iBAAsB,GAC7B,IAAI,CAYN"}
|