@reservamos/browser-analytics 1.1.6 → 1.1.7-alpha.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/CHANGELOG.md +8 -0
- package/README.md +23 -10
- package/dist/browser-analytics.cjs +3 -3
- package/dist/browser-analytics.cjs.map +1 -1
- package/dist/browser-analytics.esm.js +14 -8
- package/dist/browser-analytics.esm.js.map +1 -1
- package/dist/browser-analytics.iife.js +3 -3
- package/dist/browser-analytics.iife.js.map +1 -1
- package/package.json +14 -12
- package/src/services/fingerprint.test.ts +147 -0
- package/src/services/fingerprint.ts +17 -13
package/package.json
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reservamos/browser-analytics",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7-alpha.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/reservamos/reservamos-browser-analytics.git"
|
|
7
7
|
},
|
|
8
|
-
"main": "./dist/
|
|
9
|
-
"module": "./dist/
|
|
8
|
+
"main": "./dist/browser-analytics.cjs",
|
|
9
|
+
"module": "./dist/browser-analytics.esm.js",
|
|
10
10
|
"exports": {
|
|
11
11
|
".": {
|
|
12
12
|
"require": "./dist/browser-analytics.cjs",
|
|
13
13
|
"import": "./dist/browser-analytics.esm.js",
|
|
14
14
|
"types": "./dist/browser-analytics.d.ts"
|
|
15
15
|
},
|
|
16
|
-
"./dist
|
|
17
|
-
"import": "./dist/",
|
|
18
|
-
"require": "./dist/",
|
|
19
|
-
"types": "./dist/"
|
|
20
|
-
}
|
|
16
|
+
"./dist/*": "./dist/*"
|
|
21
17
|
},
|
|
22
18
|
"bugs": {
|
|
23
19
|
"url": "https://github.com/reservamos/reservamos-browser-analytics/issues"
|
|
@@ -32,7 +28,7 @@
|
|
|
32
28
|
"package.json"
|
|
33
29
|
],
|
|
34
30
|
"homepage": "https://github.com/reservamos/reservamos-browser-analytics#readme",
|
|
35
|
-
"jsdelivr": "./dist/
|
|
31
|
+
"jsdelivr": "./dist/browser-analytics.iife.js",
|
|
36
32
|
"keywords": [
|
|
37
33
|
"analytics",
|
|
38
34
|
"browser"
|
|
@@ -49,21 +45,27 @@
|
|
|
49
45
|
"build": "tsc && vite build",
|
|
50
46
|
"postbuild": "dts-bundle-generator --config ./dts-bundle-generator.config.ts",
|
|
51
47
|
"test": "vitest",
|
|
48
|
+
"test:run": "vitest --run",
|
|
52
49
|
"test:coverage": "vitest --coverage",
|
|
53
50
|
"format": "prettier . --write",
|
|
51
|
+
"format:check": "node scripts/check-format.mjs",
|
|
52
|
+
"lint": "eslint src/ scripts/",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"validate": "./validate.sh",
|
|
55
|
+
"validate:full": "./validate.sh --full",
|
|
54
56
|
"upgrade": "upgradeps",
|
|
55
57
|
"release": "npm run build && np"
|
|
56
58
|
},
|
|
57
59
|
"type": "module",
|
|
58
|
-
"types": "./dist/
|
|
60
|
+
"types": "./dist/browser-analytics.d.ts",
|
|
59
61
|
"typesVersions": {
|
|
60
62
|
"*": {
|
|
61
63
|
"*": [
|
|
62
|
-
"./dist/
|
|
64
|
+
"./dist/browser-analytics.d.ts"
|
|
63
65
|
]
|
|
64
66
|
}
|
|
65
67
|
},
|
|
66
|
-
"typings": "./dist/
|
|
68
|
+
"typings": "./dist/browser-analytics.d.ts",
|
|
67
69
|
"dependencies": {
|
|
68
70
|
"@fingerprintjs/fingerprintjs-pro-spa": "^1.3.3",
|
|
69
71
|
"mixpanel-browser": "^2.55.1",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import fingerprintService from './fingerprint';
|
|
3
|
+
|
|
4
|
+
const { initMock, getVisitorDataMock } = vi.hoisted(() => ({
|
|
5
|
+
initMock: vi.fn(),
|
|
6
|
+
getVisitorDataMock: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock('@fingerprintjs/fingerprintjs-pro-spa', () => ({
|
|
10
|
+
FingerprintJSPro: {
|
|
11
|
+
defaultEndpoint: 'https://fpjscdn.net',
|
|
12
|
+
defaultScriptUrlPattern: 'https://fpjscdn.net/agent',
|
|
13
|
+
},
|
|
14
|
+
FpjsClient: vi.fn().mockImplementation(() => ({
|
|
15
|
+
init: initMock,
|
|
16
|
+
getVisitorData: getVisitorDataMock,
|
|
17
|
+
})),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('fingerprintService readiness', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
localStorage.clear();
|
|
23
|
+
initMock.mockReset();
|
|
24
|
+
getVisitorDataMock.mockReset();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('does not identify with a client whose agent failed to load', async () => {
|
|
28
|
+
const initError = new Error('Agent script blocked');
|
|
29
|
+
const consoleError = vi
|
|
30
|
+
.spyOn(console, 'error')
|
|
31
|
+
.mockImplementation(() => undefined);
|
|
32
|
+
initMock.mockRejectedValue(initError);
|
|
33
|
+
getVisitorDataMock.mockRejectedValue(
|
|
34
|
+
new Error(
|
|
35
|
+
"FPJSAgent hasn't loaded yet. Make sure to call the init() method first.",
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await expect(
|
|
40
|
+
fingerprintService.initFingerprint('test-key'),
|
|
41
|
+
).rejects.toThrow('Agent script blocked');
|
|
42
|
+
expect(fingerprintService.isFingerprintReady()).toBe(false);
|
|
43
|
+
await expect(fingerprintService.getFingerprint()).resolves.toBe('');
|
|
44
|
+
expect(getVisitorDataMock).not.toHaveBeenCalled();
|
|
45
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
46
|
+
'Error initializing identification service:',
|
|
47
|
+
initError,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
consoleError.mockRestore();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('identifies after the agent loads successfully', async () => {
|
|
54
|
+
initMock.mockResolvedValue(undefined);
|
|
55
|
+
getVisitorDataMock.mockResolvedValue({ visitorId: 'visitor-123' });
|
|
56
|
+
|
|
57
|
+
await fingerprintService.initFingerprint('test-key');
|
|
58
|
+
|
|
59
|
+
expect(fingerprintService.isFingerprintReady()).toBe(true);
|
|
60
|
+
await expect(fingerprintService.getFingerprint()).resolves.toBe(
|
|
61
|
+
'visitor-123',
|
|
62
|
+
);
|
|
63
|
+
expect(getVisitorDataMock).toHaveBeenCalledOnce();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('waits for the agent to load before identifying', async () => {
|
|
67
|
+
let finishInit: () => void = () => undefined;
|
|
68
|
+
initMock.mockImplementation(
|
|
69
|
+
() =>
|
|
70
|
+
new Promise<void>((resolve) => {
|
|
71
|
+
finishInit = resolve;
|
|
72
|
+
}),
|
|
73
|
+
);
|
|
74
|
+
getVisitorDataMock.mockResolvedValue({ visitorId: 'visitor-123' });
|
|
75
|
+
|
|
76
|
+
const init = fingerprintService.initFingerprint('test-key');
|
|
77
|
+
|
|
78
|
+
expect(fingerprintService.isFingerprintReady()).toBe(false);
|
|
79
|
+
await expect(fingerprintService.getFingerprint()).resolves.toBe('');
|
|
80
|
+
expect(getVisitorDataMock).not.toHaveBeenCalled();
|
|
81
|
+
|
|
82
|
+
finishInit();
|
|
83
|
+
await init;
|
|
84
|
+
|
|
85
|
+
expect(fingerprintService.isFingerprintReady()).toBe(true);
|
|
86
|
+
await expect(fingerprintService.getFingerprint()).resolves.toBe(
|
|
87
|
+
'visitor-123',
|
|
88
|
+
);
|
|
89
|
+
expect(getVisitorDataMock).toHaveBeenCalledOnce();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('does not mark a newer failed client ready when an older init finishes', async () => {
|
|
93
|
+
let finishFirstInit: () => void = () => undefined;
|
|
94
|
+
initMock
|
|
95
|
+
.mockImplementationOnce(
|
|
96
|
+
() =>
|
|
97
|
+
new Promise<void>((resolve) => {
|
|
98
|
+
finishFirstInit = resolve;
|
|
99
|
+
}),
|
|
100
|
+
)
|
|
101
|
+
.mockRejectedValueOnce(new Error('New agent script blocked'));
|
|
102
|
+
const consoleError = vi
|
|
103
|
+
.spyOn(console, 'error')
|
|
104
|
+
.mockImplementation(() => undefined);
|
|
105
|
+
|
|
106
|
+
const firstInit = fingerprintService.initFingerprint('first-key');
|
|
107
|
+
await expect(
|
|
108
|
+
fingerprintService.initFingerprint('second-key'),
|
|
109
|
+
).rejects.toThrow('New agent script blocked');
|
|
110
|
+
finishFirstInit();
|
|
111
|
+
await firstInit;
|
|
112
|
+
|
|
113
|
+
expect(fingerprintService.isFingerprintReady()).toBe(false);
|
|
114
|
+
await expect(fingerprintService.getFingerprint()).resolves.toBe('');
|
|
115
|
+
expect(getVisitorDataMock).not.toHaveBeenCalled();
|
|
116
|
+
|
|
117
|
+
consoleError.mockRestore();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('keeps a newer loaded client ready when an older init fails', async () => {
|
|
121
|
+
let failFirstInit: (reason?: unknown) => void = () => undefined;
|
|
122
|
+
initMock
|
|
123
|
+
.mockImplementationOnce(
|
|
124
|
+
() =>
|
|
125
|
+
new Promise<void>((_resolve, reject) => {
|
|
126
|
+
failFirstInit = reject;
|
|
127
|
+
}),
|
|
128
|
+
)
|
|
129
|
+
.mockResolvedValueOnce(undefined);
|
|
130
|
+
getVisitorDataMock.mockResolvedValue({ visitorId: 'new-visitor' });
|
|
131
|
+
const consoleError = vi
|
|
132
|
+
.spyOn(console, 'error')
|
|
133
|
+
.mockImplementation(() => undefined);
|
|
134
|
+
|
|
135
|
+
const firstInit = fingerprintService.initFingerprint('first-key');
|
|
136
|
+
await fingerprintService.initFingerprint('second-key');
|
|
137
|
+
failFirstInit(new Error('Old agent script blocked'));
|
|
138
|
+
await expect(firstInit).rejects.toThrow('Old agent script blocked');
|
|
139
|
+
|
|
140
|
+
expect(fingerprintService.isFingerprintReady()).toBe(true);
|
|
141
|
+
await expect(fingerprintService.getFingerprint()).resolves.toBe(
|
|
142
|
+
'new-visitor',
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
consoleError.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -18,6 +18,7 @@ const DEFAULT_CACHE_NAME = 'default_fingerprint_cache';
|
|
|
18
18
|
const DEFAULT_CACHE_TIME_IN_DAYS = 7;
|
|
19
19
|
const PROXY_QUERY_PARAM =
|
|
20
20
|
'apiKey=<apiKey>&version=<version>&loaderVersion=<loaderVersion>';
|
|
21
|
+
let fingerprintAgentReady = false;
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Calculates the expiration date based on the cache time.
|
|
@@ -102,18 +103,26 @@ async function initFingerprint(
|
|
|
102
103
|
customScriptEndpointArray.unshift(buildProxyUrl(proxyUrl));
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
fingerprintAgentReady = false;
|
|
105
107
|
window.fingerprintConfig = {};
|
|
106
|
-
|
|
108
|
+
const client = new FpjsClient({
|
|
107
109
|
loadOptions: {
|
|
108
110
|
apiKey,
|
|
109
111
|
endpoint: customEndpointArray,
|
|
110
112
|
scriptUrlPattern: customScriptEndpointArray,
|
|
111
113
|
},
|
|
112
114
|
});
|
|
115
|
+
window.fpClient = client;
|
|
113
116
|
|
|
114
117
|
try {
|
|
115
|
-
await
|
|
118
|
+
await client.init();
|
|
119
|
+
if (window.fpClient === client) {
|
|
120
|
+
fingerprintAgentReady = true;
|
|
121
|
+
}
|
|
116
122
|
} catch (error) {
|
|
123
|
+
if (window.fpClient === client) {
|
|
124
|
+
fingerprintAgentReady = false;
|
|
125
|
+
}
|
|
117
126
|
console.error('Error initializing identification service:', error);
|
|
118
127
|
throw error;
|
|
119
128
|
}
|
|
@@ -122,8 +131,8 @@ async function initFingerprint(
|
|
|
122
131
|
/**
|
|
123
132
|
* Retrieves the fingerprint, first checking the cache and then querying the identification service if not found.
|
|
124
133
|
* @param {boolean} cacheOnly - Optional flag to retrieve the fingerprint only from the cache.
|
|
125
|
-
* @returns {Promise<string>} A promise that resolves with the fingerprint.
|
|
126
|
-
* @throws {Error} Throws an error if retrieval fails.
|
|
134
|
+
* @returns {Promise<string>} A promise that resolves with the fingerprint, or an empty string when the agent is unavailable.
|
|
135
|
+
* @throws {Error} Throws an error if retrieval fails after the agent has loaded.
|
|
127
136
|
*/
|
|
128
137
|
async function getFingerprint(cacheOnly = false): Promise<string> {
|
|
129
138
|
if (!window.fingerprintConfig) {
|
|
@@ -139,14 +148,10 @@ async function getFingerprint(cacheOnly = false): Promise<string> {
|
|
|
139
148
|
return cachedFingerprint;
|
|
140
149
|
}
|
|
141
150
|
|
|
142
|
-
if (cacheOnly) {
|
|
151
|
+
if (cacheOnly || !isFingerprintReady()) {
|
|
143
152
|
return '';
|
|
144
153
|
}
|
|
145
154
|
|
|
146
|
-
if (!isFingerprintReady()) {
|
|
147
|
-
throw new Error('Identification service is not initialized.');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
155
|
try {
|
|
151
156
|
const fpClient = window.fpClient;
|
|
152
157
|
const { visitorId: fingerprint } = await fpClient.getVisitorData();
|
|
@@ -161,12 +166,11 @@ async function getFingerprint(cacheOnly = false): Promise<string> {
|
|
|
161
166
|
/**
|
|
162
167
|
* Checks if the identification service is ready.
|
|
163
168
|
*
|
|
164
|
-
* This function verifies
|
|
165
|
-
*
|
|
166
|
-
* @returns {boolean} Returns true if the identification service is ready, otherwise false.
|
|
169
|
+
* This function verifies that the Fingerprint agent loaded successfully.
|
|
170
|
+
* @returns {boolean} Returns true only after the agent has loaded.
|
|
167
171
|
*/
|
|
168
172
|
function isFingerprintReady(): boolean {
|
|
169
|
-
return window.fpClient !== undefined;
|
|
173
|
+
return window.fpClient !== undefined && fingerprintAgentReady;
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
const fingerprintService = {
|