@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/package.json CHANGED
@@ -1,23 +1,19 @@
1
1
  {
2
2
  "name": "@reservamos/browser-analytics",
3
- "version": "1.1.6",
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/reservamos-browser-analytics.cjs",
9
- "module": "./dist/reservamos-browser-analytics.esm.js",
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/reservamos-browser-analytics.iife.js",
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/reservamos-browser-analytics.d.ts",
60
+ "types": "./dist/browser-analytics.d.ts",
59
61
  "typesVersions": {
60
62
  "*": {
61
63
  "*": [
62
- "./dist/reservamos-browser-analytics.d.ts"
64
+ "./dist/browser-analytics.d.ts"
63
65
  ]
64
66
  }
65
67
  },
66
- "typings": "./dist/reservamos-browser-analytics.d.ts",
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
- window.fpClient = new FpjsClient({
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 window.fpClient.init();
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 whether the identification service has been initialized
165
- * and is ready to use by checking if the `fpClient` instance exists on the window object.
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 = {