@thumbmarkjs/thumbmarkjs 1.7.1 → 1.7.3
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/thumbmark.cjs.js +1 -1
- package/dist/thumbmark.cjs.js.map +1 -1
- package/dist/thumbmark.esm.d.ts +8 -3
- package/dist/thumbmark.esm.js +1 -1
- package/dist/thumbmark.esm.js.map +1 -1
- package/dist/thumbmark.umd.js +1 -1
- package/dist/thumbmark.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/components/system/browser.test.ts +0 -5
- package/src/components/system/browser.ts +29 -5
- package/src/functions/api.ts +2 -8
- package/src/functions/functions.test.ts +85 -2
- package/src/functions/index.ts +106 -69
- package/src/index.ts +2 -2
- package/src/utils/log.ts +3 -1
- package/src/utils/raceAll.test.ts +4 -0
- package/src/utils/raceAll.ts +4 -3
package/src/functions/index.ts
CHANGED
|
@@ -27,6 +27,12 @@ import { stableStringify } from "../utils/stableStringify";
|
|
|
27
27
|
/**
|
|
28
28
|
* Final thumbmark response structure
|
|
29
29
|
*/
|
|
30
|
+
export interface ThumbmarkError {
|
|
31
|
+
type: 'component_timeout' | 'component_error' | 'api_timeout' | 'api_error' | 'api_unauthorized' | 'fatal';
|
|
32
|
+
message: string;
|
|
33
|
+
component?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
export interface ThumbmarkResponse {
|
|
31
37
|
/** Hash of all components - the main fingerprint identifier */
|
|
32
38
|
thumbmark: string;
|
|
@@ -40,8 +46,8 @@ export interface ThumbmarkResponse {
|
|
|
40
46
|
visitorId?: string;
|
|
41
47
|
/** Performance timing for each component (only when options.performance is true) */
|
|
42
48
|
elapsed?: Record<string, number>;
|
|
43
|
-
/**
|
|
44
|
-
error?:
|
|
49
|
+
/** Structured error array. Present only when errors occurred. */
|
|
50
|
+
error?: ThumbmarkError[];
|
|
45
51
|
/** Experimental components (only when options.experimental is true) */
|
|
46
52
|
experimental?: componentInterface;
|
|
47
53
|
/** Unique identifier for this API request */
|
|
@@ -64,78 +70,100 @@ export async function getThumbmark(options?: optionsInterface): Promise<Thumbmar
|
|
|
64
70
|
components: {},
|
|
65
71
|
info: {},
|
|
66
72
|
version: getVersion(),
|
|
67
|
-
error: 'Browser environment required'
|
|
73
|
+
error: [{ type: 'fatal', message: 'Browser environment required' }]
|
|
68
74
|
};
|
|
69
75
|
}
|
|
70
76
|
|
|
71
|
-
|
|
77
|
+
try {
|
|
78
|
+
const _options = { ...defaultOptions, ...options } as OptionsAfterDefaults;
|
|
79
|
+
const allErrors: ThumbmarkError[] = [];
|
|
80
|
+
|
|
81
|
+
// Early logging decision
|
|
82
|
+
const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
|
|
83
|
+
|
|
84
|
+
// Merge built-in and user-registered components
|
|
85
|
+
const allComponents = { ...tm_component_promises, ...customComponents };
|
|
86
|
+
const { elapsed, resolvedComponents: clientComponentsResult, errors: componentErrors } = await resolveClientComponents(allComponents, _options);
|
|
87
|
+
allErrors.push(...componentErrors);
|
|
88
|
+
|
|
89
|
+
// Resolve experimental components only when logging
|
|
90
|
+
let experimentalComponents = {};
|
|
91
|
+
let experimentalElapsed = {};
|
|
92
|
+
if (shouldLog || _options.experimental) {
|
|
93
|
+
const { elapsed: expElapsed, resolvedComponents, errors: expErrors } = await resolveClientComponents(tm_experimental_component_promises, _options);
|
|
94
|
+
experimentalComponents = resolvedComponents;
|
|
95
|
+
experimentalElapsed = expElapsed;
|
|
96
|
+
allErrors.push(...expErrors);
|
|
97
|
+
}
|
|
72
98
|
|
|
73
|
-
|
|
74
|
-
|
|
99
|
+
const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
|
|
100
|
+
let apiResult = null;
|
|
101
|
+
|
|
102
|
+
if (apiPromise) {
|
|
103
|
+
try {
|
|
104
|
+
apiResult = await apiPromise;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof Error && error.message === 'INVALID_API_KEY') {
|
|
107
|
+
return {
|
|
108
|
+
error: [{ type: 'api_unauthorized', message: 'Invalid API key or quota exceeded' }],
|
|
109
|
+
components: {},
|
|
110
|
+
info: {},
|
|
111
|
+
version: getVersion(),
|
|
112
|
+
thumbmark: ''
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
// Non-auth API errors (5xx, network): log error and continue without API data
|
|
116
|
+
allErrors.push({
|
|
117
|
+
type: 'api_error',
|
|
118
|
+
message: error instanceof Error ? error.message : String(error)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
75
122
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
123
|
+
// Surface API timeout as a structured error
|
|
124
|
+
if (apiResult?.info?.timed_out) {
|
|
125
|
+
allErrors.push({ type: 'api_timeout', message: 'API request timed out' });
|
|
126
|
+
}
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const {
|
|
85
|
-
|
|
86
|
-
experimentalElapsed = expElapsed;
|
|
87
|
-
}
|
|
128
|
+
// Only add 'elapsed' if performance is true
|
|
129
|
+
const allElapsed = { ...elapsed, ...experimentalElapsed };
|
|
130
|
+
const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
|
|
131
|
+
const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
|
|
132
|
+
const components = { ...clientComponentsResult, ...apiComponents };
|
|
133
|
+
const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
|
|
88
134
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (apiPromise) {
|
|
93
|
-
try {
|
|
94
|
-
apiResult = await apiPromise;
|
|
95
|
-
} catch (error) {
|
|
96
|
-
// Handle API key/quota errors
|
|
97
|
-
if (error instanceof Error && error.message === 'INVALID_API_KEY') {
|
|
98
|
-
return {
|
|
99
|
-
error: 'Invalid API key or quota exceeded',
|
|
100
|
-
components: {},
|
|
101
|
-
info: {},
|
|
102
|
-
version: getVersion(),
|
|
103
|
-
thumbmark: ''
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
throw error; // Re-throw other errors
|
|
107
|
-
}
|
|
108
|
-
}
|
|
135
|
+
// Use API thumbmark if available to ensure API/client sync, otherwise calculate locally
|
|
136
|
+
const thumbmark = apiResult?.thumbmark ?? hash(stableStringify(components));
|
|
137
|
+
const version = getVersion();
|
|
109
138
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const components = { ...clientComponentsResult, ...apiComponents };
|
|
115
|
-
const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
|
|
139
|
+
// Only log to server when not in debug mode
|
|
140
|
+
if (shouldLog) {
|
|
141
|
+
logThumbmarkData(thumbmark, components, _options, experimentalComponents, allErrors).catch(() => { /* do nothing */ });
|
|
142
|
+
}
|
|
116
143
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
const result: ThumbmarkResponse = {
|
|
145
|
+
...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
|
|
146
|
+
thumbmark,
|
|
147
|
+
components: components,
|
|
148
|
+
info,
|
|
149
|
+
version,
|
|
150
|
+
...maybeElapsed,
|
|
151
|
+
...(allErrors.length > 0 && { error: allErrors }),
|
|
152
|
+
...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
|
|
153
|
+
...(apiResult?.requestId && { requestId: apiResult.requestId }),
|
|
154
|
+
...(apiResult?.metadata && { metadata: apiResult.metadata }),
|
|
155
|
+
};
|
|
120
156
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
157
|
+
return result;
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return {
|
|
160
|
+
thumbmark: '',
|
|
161
|
+
components: {},
|
|
162
|
+
info: {},
|
|
163
|
+
version: getVersion(),
|
|
164
|
+
error: [{ type: 'fatal', message: e instanceof Error ? e.message : String(e) }],
|
|
165
|
+
};
|
|
124
166
|
}
|
|
125
|
-
|
|
126
|
-
const result: ThumbmarkResponse = {
|
|
127
|
-
...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
|
|
128
|
-
thumbmark,
|
|
129
|
-
components: components,
|
|
130
|
-
info,
|
|
131
|
-
version,
|
|
132
|
-
...maybeElapsed,
|
|
133
|
-
...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
|
|
134
|
-
...(apiResult?.requestId && { requestId: apiResult.requestId }),
|
|
135
|
-
...(apiResult?.metadata && { metadata: apiResult.metadata }),
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
return result;
|
|
139
167
|
}
|
|
140
168
|
|
|
141
169
|
// ===================== Component Resolution & Performance =====================
|
|
@@ -150,7 +178,7 @@ export async function getThumbmark(options?: optionsInterface): Promise<Thumbmar
|
|
|
150
178
|
export async function resolveClientComponents(
|
|
151
179
|
comps: { [key: string]: (options?: optionsInterface) => Promise<componentInterface | null> },
|
|
152
180
|
options?: optionsInterface
|
|
153
|
-
): Promise<{ elapsed: Record<string, number>, resolvedComponents: componentInterface }> {
|
|
181
|
+
): Promise<{ elapsed: Record<string, number>, resolvedComponents: componentInterface, errors: ThumbmarkError[] }> {
|
|
154
182
|
const opts = { ...defaultOptions, ...options };
|
|
155
183
|
const filtered = Object.entries(comps)
|
|
156
184
|
.filter(([key]) => !opts?.exclude?.includes(key))
|
|
@@ -165,16 +193,25 @@ export async function resolveClientComponents(
|
|
|
165
193
|
|
|
166
194
|
const elapsed: Record<string, number> = {};
|
|
167
195
|
const resolvedComponentsRaw: Record<string, componentInterface> = {};
|
|
196
|
+
const errors: ThumbmarkError[] = [];
|
|
197
|
+
|
|
198
|
+
resolvedValues.forEach((result, index) => {
|
|
199
|
+
const key = keys[index];
|
|
200
|
+
elapsed[key] = result.elapsed ?? 0;
|
|
201
|
+
|
|
202
|
+
if (result.error === 'timeout') {
|
|
203
|
+
errors.push({ type: 'component_timeout', message: `Component '${key}' timed out`, component: key });
|
|
204
|
+
} else if (result.error) {
|
|
205
|
+
errors.push({ type: 'component_error', message: result.error, component: key });
|
|
206
|
+
}
|
|
168
207
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
resolvedComponentsRaw[keys[index]] = value.value;
|
|
172
|
-
elapsed[keys[index]] = value.elapsed ?? 0;
|
|
208
|
+
if (result.value != null) {
|
|
209
|
+
resolvedComponentsRaw[key] = result.value;
|
|
173
210
|
}
|
|
174
211
|
});
|
|
175
212
|
|
|
176
213
|
const resolvedComponents = filterThumbmarkData(resolvedComponentsRaw, opts);
|
|
177
|
-
return { elapsed, resolvedComponents };
|
|
214
|
+
return { elapsed, resolvedComponents, errors };
|
|
178
215
|
}
|
|
179
216
|
|
|
180
217
|
export { globalIncludeComponent as includeComponent };
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
getFingerprintData,
|
|
4
4
|
getFingerprintPerformance
|
|
5
5
|
} from './functions/legacy_functions'
|
|
6
|
-
import { getThumbmark, ThumbmarkResponse } from './functions'
|
|
6
|
+
import { getThumbmark, ThumbmarkResponse, ThumbmarkError } from './functions'
|
|
7
7
|
import { getVersion } from './utils/version';
|
|
8
8
|
import { setOption, optionsInterface, stabilizationExclusionRules } from './options'
|
|
9
9
|
import { includeComponent } from './factory'
|
|
@@ -12,7 +12,7 @@ import { filterThumbmarkData } from './functions/filterComponents'
|
|
|
12
12
|
import { stableStringify } from './utils/stableStringify'
|
|
13
13
|
|
|
14
14
|
export {
|
|
15
|
-
Thumbmark, getThumbmark, getVersion, ThumbmarkResponse,
|
|
15
|
+
Thumbmark, getThumbmark, getVersion, ThumbmarkResponse, ThumbmarkError,
|
|
16
16
|
|
|
17
17
|
// Filtering functions for server-side use
|
|
18
18
|
filterThumbmarkData, optionsInterface, stabilizationExclusionRules,
|
package/src/utils/log.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { componentInterface } from '../factory';
|
|
2
2
|
import { optionsInterface, DEFAULT_API_ENDPOINT } from '../options';
|
|
3
3
|
import { getVersion } from './version';
|
|
4
|
+
import type { ThumbmarkError } from '../functions';
|
|
4
5
|
|
|
5
6
|
// ===================== Logging (Internal) =====================
|
|
6
7
|
|
|
@@ -9,7 +10,7 @@ import { getVersion } from './version';
|
|
|
9
10
|
* You can disable this by setting options.logging to false.
|
|
10
11
|
* @internal
|
|
11
12
|
*/
|
|
12
|
-
export async function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface, experimentalData: componentInterface = {}): Promise<void> {
|
|
13
|
+
export async function logThumbmarkData(thisHash: string, thumbmarkData: componentInterface, options: optionsInterface, experimentalData: componentInterface = {}, errors: ThumbmarkError[] = []): Promise<void> {
|
|
13
14
|
const apiEndpoint = DEFAULT_API_ENDPOINT;
|
|
14
15
|
const url = `${apiEndpoint}/log`;
|
|
15
16
|
const payload = {
|
|
@@ -19,6 +20,7 @@ export async function logThumbmarkData(thisHash: string, thumbmarkData: componen
|
|
|
19
20
|
version: getVersion(),
|
|
20
21
|
options,
|
|
21
22
|
path: window?.location?.pathname,
|
|
23
|
+
...(errors.length > 0 && { errors }),
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
sessionStorage.setItem("_tmjs_l", "1");
|
|
@@ -51,6 +51,7 @@ describe('raceAllPerformance', () => {
|
|
|
51
51
|
expect(results).toHaveLength(1);
|
|
52
52
|
expect(results[0].value).toBe('ok');
|
|
53
53
|
expect(typeof results[0].elapsed).toBe('number');
|
|
54
|
+
expect(results[0].error).toBeUndefined();
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
test('returns timeout fallback when promise does not settle in time', async () => {
|
|
@@ -63,6 +64,7 @@ describe('raceAllPerformance', () => {
|
|
|
63
64
|
|
|
64
65
|
expect(results).toHaveLength(1);
|
|
65
66
|
expect(results[0].value).toBe('timeout');
|
|
67
|
+
expect(results[0].error).toBe('timeout');
|
|
66
68
|
});
|
|
67
69
|
|
|
68
70
|
test('does not reject the whole batch when one promise rejects', async () => {
|
|
@@ -77,6 +79,8 @@ describe('raceAllPerformance', () => {
|
|
|
77
79
|
|
|
78
80
|
expect(results).toHaveLength(2);
|
|
79
81
|
expect(results[0].value).toBe('timeout');
|
|
82
|
+
expect(results[0].error).toBe('component failed');
|
|
80
83
|
expect(results[1].value).toBe('ok');
|
|
84
|
+
expect(results[1].error).toBeUndefined();
|
|
81
85
|
});
|
|
82
86
|
});
|
package/src/utils/raceAll.ts
CHANGED
|
@@ -10,6 +10,7 @@ export function delay<T>(t: number, val: T): DelayedPromise<T> {
|
|
|
10
10
|
export interface RaceResult<T> {
|
|
11
11
|
value: T;
|
|
12
12
|
elapsed?: number;
|
|
13
|
+
error?: string;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
export function raceAllPerformance<T>(
|
|
@@ -24,15 +25,15 @@ export function raceAllPerformance<T>(
|
|
|
24
25
|
p.then((value) => ({
|
|
25
26
|
value,
|
|
26
27
|
elapsed: performance.now() - startTime,
|
|
27
|
-
})).catch(() => ({
|
|
28
|
-
// Keep behavior aligned with timeout: a failed component falls back
|
|
29
|
-
// to timeoutVal instead of rejecting the entire Promise.all.
|
|
28
|
+
})).catch((err: unknown) => ({
|
|
30
29
|
value: timeoutVal,
|
|
31
30
|
elapsed: performance.now() - startTime,
|
|
31
|
+
error: err instanceof Error ? err.message : String(err),
|
|
32
32
|
})),
|
|
33
33
|
delay(timeoutTime, timeoutVal).then((value) => ({
|
|
34
34
|
value,
|
|
35
35
|
elapsed: performance.now() - startTime,
|
|
36
|
+
error: 'timeout' as string,
|
|
36
37
|
})),
|
|
37
38
|
]);
|
|
38
39
|
})
|