@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.
@@ -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
- /** Error message if something went wrong */
44
- error?: string;
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
- const _options = { ...defaultOptions, ...options } as OptionsAfterDefaults;
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
- // Early logging decision
74
- const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
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
- // Merge built-in and user-registered components
77
- const allComponents = { ...tm_component_promises, ...customComponents };
78
- const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options);
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
- // Resolve experimental components only when logging
81
- let experimentalComponents = {};
82
- let experimentalElapsed = {};
83
- if (shouldLog || _options.experimental) {
84
- const { elapsed: expElapsed, resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
85
- experimentalComponents = resolvedComponents;
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
- const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
90
- let apiResult = null;
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
- // Only add 'elapsed' if performance is true
111
- const allElapsed = { ...elapsed, ...experimentalElapsed };
112
- const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
113
- const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
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
- // Use API thumbmark if available to ensure API/client sync, otherwise calculate locally
118
- const thumbmark = apiResult?.thumbmark ?? hash(stableStringify(components));
119
- const version = getVersion();
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
- // Only log to server when not in debug mode
122
- if (shouldLog) {
123
- logThumbmarkData(thumbmark, components, _options, experimentalComponents).catch(() => { /* do nothing */ });
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
- resolvedValues.forEach((value, index) => {
170
- if (value.value != null) {
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
  });
@@ -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
  })