@thumbmarkjs/thumbmarkjs 1.2.0 → 1.3.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.
Files changed (46) hide show
  1. package/README.md +3 -2
  2. package/dist/thumbmark.cjs.js +1 -1
  3. package/dist/thumbmark.cjs.js.map +1 -1
  4. package/dist/thumbmark.esm.d.ts +3 -10
  5. package/dist/thumbmark.esm.js +1 -1
  6. package/dist/thumbmark.esm.js.map +1 -1
  7. package/dist/thumbmark.umd.js +1 -1
  8. package/dist/thumbmark.umd.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/components/mathml/index.ts +149 -0
  11. package/src/components/webrtc/index.ts +126 -0
  12. package/src/factory.ts +12 -0
  13. package/src/functions/api.ts +130 -0
  14. package/src/functions/filterComponents.ts +0 -1
  15. package/src/functions/index.ts +39 -140
  16. package/src/options.ts +3 -1
  17. package/src/utils/log.ts +2 -1
  18. package/dist/types/components/audio/index.d.ts +0 -2
  19. package/dist/types/components/canvas/index.d.ts +0 -3
  20. package/dist/types/components/fonts/index.d.ts +0 -4
  21. package/dist/types/components/hardware/index.d.ts +0 -2
  22. package/dist/types/components/locales/index.d.ts +0 -2
  23. package/dist/types/components/math/index.d.ts +0 -2
  24. package/dist/types/components/permissions/index.d.ts +0 -3
  25. package/dist/types/components/plugins/index.d.ts +0 -2
  26. package/dist/types/components/screen/index.d.ts +0 -2
  27. package/dist/types/components/system/browser.d.ts +0 -7
  28. package/dist/types/components/system/index.d.ts +0 -2
  29. package/dist/types/components/webgl/index.d.ts +0 -2
  30. package/dist/types/factory.d.ts +0 -51
  31. package/dist/types/functions/filterComponents.d.ts +0 -10
  32. package/dist/types/functions/index.d.ts +0 -89
  33. package/dist/types/functions/legacy_functions.d.ts +0 -27
  34. package/dist/types/index.d.ts +0 -8
  35. package/dist/types/options.d.ts +0 -43
  36. package/dist/types/thumbmark.d.ts +0 -26
  37. package/dist/types/utils/commonPixels.d.ts +0 -1
  38. package/dist/types/utils/ephemeralIFrame.d.ts +0 -4
  39. package/dist/types/utils/getMostFrequent.d.ts +0 -5
  40. package/dist/types/utils/hash.d.ts +0 -5
  41. package/dist/types/utils/imageDataToDataURL.d.ts +0 -1
  42. package/dist/types/utils/log.d.ts +0 -8
  43. package/dist/types/utils/raceAll.d.ts +0 -9
  44. package/dist/types/utils/sort.d.ts +0 -8
  45. package/dist/types/utils/version.d.ts +0 -4
  46. package/dist/types/utils/visitorId.d.ts +0 -11
@@ -0,0 +1,149 @@
1
+ import { componentInterface } from '../../factory';
2
+ import { hash } from '../../utils/hash';
3
+
4
+ const BLACKBOARD_BOLD = ['\uD835\uDD04', '\uD835\uDD05', '\u212D', '\uD835\uDD07', '\uD835\uDD08', '\uD835\uDD09', '\uD835\uDD38', '\uD835\uDD39', '\u2102', '\uD835\uDD3B', '\uD835\uDD3C', '\uD835\uDD3D'];
5
+ const GREEK_SYMBOLS = ['\u03B2', '\u03C8', '\u03BB', '\u03B5', '\u03B6', '\u03B1', '\u03BE', '\u03BC', '\u03C1', '\u03C6', '\u03BA', '\u03C4', '\u03B7', '\u03C3', '\u03B9', '\u03C9', '\u03B3', '\u03BD', '\u03C7', '\u03B4', '\u03B8', '\u03C0', '\u03C5', '\u03BF'];
6
+
7
+ export default async function getMathML(): Promise<componentInterface | null> {
8
+ return new Promise((resolve) => {
9
+ try {
10
+ if (!isMathMLSupported()) {
11
+ resolve({
12
+ supported: false,
13
+ error: 'MathML not supported'
14
+ });
15
+ return;
16
+ }
17
+
18
+ const structures = [
19
+ createMathML('integral', '<msubsup><mo>\u222B</mo><mi>a</mi><mi>b</mi></msubsup><mfrac><mrow><mi>f</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mi>g</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow></mfrac><mi>dx</mi>'),
20
+ createMathML('fraction', '<mfrac><mrow><mi>\u03C0</mi><mo>\u00D7</mo><msup><mi>r</mi><mn>2</mn></msup></mrow><mrow><mn>2</mn><mi>\u03C3</mi></mrow></mfrac>'),
21
+ createMathML('matrix', '<mo>[</mo><mtable><mtr><mtd><mi>\u03B1</mi></mtd><mtd><mi>\u03B2</mi></mtd></mtr><mtr><mtd><mi>\u03B3</mi></mtd><mtd><mi>\u03B4</mi></mtd></mtr></mtable><mo>]</mo>'),
22
+ createComplexNestedStructure(),
23
+ ...createSymbolStructures()
24
+ ];
25
+
26
+ const measurements: any = {};
27
+ structures.forEach((struct, i) => {
28
+ measurements[`struct_${i}`] = measureMathMLStructure(struct);
29
+ });
30
+
31
+ resolve({
32
+ //supported: true,
33
+ //measurements,
34
+ hash: hash(JSON.stringify(measurements))
35
+ });
36
+
37
+ } catch (error) {
38
+ resolve({
39
+ supported: false,
40
+ error: `MathML error: ${(error as Error).message}`
41
+ });
42
+ }
43
+ });
44
+ }
45
+
46
+ function isMathMLSupported(): boolean {
47
+ try {
48
+ const testElement = document.createElement('math');
49
+ testElement.innerHTML = '<mrow><mi>x</mi></mrow>';
50
+ testElement.style.position = 'absolute';
51
+ testElement.style.visibility = 'hidden';
52
+
53
+ document.body.appendChild(testElement);
54
+ const rect = testElement.getBoundingClientRect();
55
+ document.body.removeChild(testElement);
56
+
57
+ return rect.width > 0 && rect.height > 0;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ function createMathML(name: string, content: string): string {
64
+ return `<math><mrow>${content}</mrow></math>`;
65
+ }
66
+
67
+ function createComplexNestedStructure(): string {
68
+ let nestedContent = '<mo>\u220F</mo>'; // Product symbol (∏)
69
+
70
+ // Add all symbol combinations inside the main structure
71
+ BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
72
+ const startIdx = bbIndex * 2;
73
+ const greekSet = GREEK_SYMBOLS.slice(startIdx, startIdx + 2);
74
+
75
+ if (greekSet.length === 2) {
76
+ nestedContent += `<mmultiscripts><mi>${bbSymbol}</mi><none/><mi>${greekSet[1]}</mi><mprescripts></mprescripts><mi>${greekSet[0]}</mi><none/></mmultiscripts>`;
77
+ }
78
+ });
79
+
80
+ return createMathML('complex_nested',
81
+ `<munderover><mmultiscripts>${nestedContent}</mmultiscripts></munderover>`
82
+ );
83
+ }
84
+
85
+ function createSymbolStructures(): string[] {
86
+ const structures: string[] = [];
87
+
88
+ // Use blackboard bold as base symbols with Greek symbols as subscripts/superscripts
89
+ BLACKBOARD_BOLD.forEach((bbSymbol, bbIndex) => {
90
+ // Get 2 Greek symbols for this blackboard bold symbol (lower left, top right)
91
+ const startIdx = bbIndex * 2;
92
+ const greekSet = GREEK_SYMBOLS.slice(startIdx, startIdx + 2);
93
+
94
+ if (greekSet.length === 2) {
95
+ structures.push(createMathML('combined',
96
+ `<mmultiscripts><mi>${bbSymbol}</mi><none/><mi>${greekSet[1]}</mi><mprescripts></mprescripts><mi>${greekSet[0]}</mi><none/></mmultiscripts>`
97
+ ));
98
+ }
99
+ });
100
+
101
+ return structures;
102
+ }
103
+
104
+ function measureMathMLStructure(mathml: string): any {
105
+ try {
106
+ const mathElement = document.createElement('math');
107
+ mathElement.innerHTML = mathml.replace(/<\/?math>/g, '');
108
+ mathElement.style.whiteSpace = 'nowrap';
109
+ mathElement.style.position = 'absolute';
110
+ mathElement.style.visibility = 'hidden';
111
+ mathElement.style.top = '-9999px';
112
+
113
+ document.body.appendChild(mathElement);
114
+
115
+ const rect = mathElement.getBoundingClientRect();
116
+ const computedStyle = window.getComputedStyle(mathElement);
117
+
118
+ const measurements = {
119
+ dimensions: {
120
+ width: rect.width,
121
+ height: rect.height,
122
+
123
+ },
124
+ fontInfo: {
125
+ fontFamily: computedStyle.fontFamily,
126
+ fontSize: computedStyle.fontSize,
127
+ fontWeight: computedStyle.fontWeight,
128
+ fontStyle: computedStyle.fontStyle,
129
+ lineHeight: computedStyle.lineHeight,
130
+ // Enhanced font properties for better system detection
131
+ fontVariant: computedStyle.fontVariant || 'normal',
132
+ fontStretch: computedStyle.fontStretch || 'normal',
133
+ fontSizeAdjust: computedStyle.fontSizeAdjust || 'none',
134
+ textRendering: computedStyle.textRendering || 'auto',
135
+ fontFeatureSettings: computedStyle.fontFeatureSettings || 'normal',
136
+ fontVariantNumeric: computedStyle.fontVariantNumeric || 'normal',
137
+ fontKerning: computedStyle.fontKerning || 'auto'
138
+ }
139
+ };
140
+
141
+ document.body.removeChild(mathElement);
142
+ return measurements;
143
+
144
+ } catch (error) {
145
+ return {
146
+ error: (error as Error).message
147
+ };
148
+ }
149
+ }
@@ -0,0 +1,126 @@
1
+ import { componentInterface } from '../../factory';
2
+ import { hash } from '../../utils/hash';
3
+
4
+ export default async function getWebRTC(): Promise<componentInterface | null> {
5
+ return new Promise((resolve) => {
6
+ try {
7
+ // Check if WebRTC is supported
8
+ const RTCPeerConnection = (window as any).RTCPeerConnection || (window as any).webkitRTCPeerConnection || (window as any).mozRTCPeerConnection;
9
+ if (!RTCPeerConnection) {
10
+ resolve({
11
+ supported: false,
12
+ error: 'WebRTC not supported'
13
+ });
14
+ return;
15
+ }
16
+
17
+ const config = {
18
+ iceCandidatePoolSize: 1,
19
+ iceServers: []
20
+ };
21
+
22
+ const connection = new RTCPeerConnection(config);
23
+ connection.createDataChannel(''); // trigger ICE gathering
24
+
25
+ const processOffer = async () => {
26
+ try {
27
+ const offerOptions = { offerToReceiveAudio: true, offerToReceiveVideo: true };
28
+ const offer = await connection.createOffer(offerOptions);
29
+ await connection.setLocalDescription(offer);
30
+
31
+ const sdp = offer.sdp || '';
32
+
33
+ // Extract RTP extensions
34
+ const extensions = [...new Set((sdp.match(/extmap:\d+ [^\n\r]+/g) || []).map((x: string) => x.replace(/extmap:\d+ /, '')))].sort();
35
+
36
+ // Extract codec information
37
+ const getDescriptors = (mediaType: string) => {
38
+ const match = sdp.match(new RegExp(`m=${mediaType} [^\\s]+ [^\\s]+ ([^\\n\\r]+)`));
39
+ return match ? match[1].split(' ') : [];
40
+ };
41
+
42
+ const constructDescriptions = (mediaType: string, descriptors: string[]) => {
43
+ return descriptors.map(descriptor => {
44
+ const matcher = new RegExp(`(rtpmap|fmtp|rtcp-fb):${descriptor} (.+)`, 'g');
45
+ const matches = [...sdp.matchAll(matcher)];
46
+ if (!matches.length) return null;
47
+
48
+ const description: any = {};
49
+ matches.forEach(match => {
50
+ const [_, type, data] = match;
51
+ const parts = data.split('/');
52
+ if (type === 'rtpmap') {
53
+ description.mimeType = `${mediaType}/${parts[0]}`;
54
+ description.clockRate = +parts[1];
55
+ if (mediaType === 'audio') description.channels = +parts[2] || 1;
56
+ } else if (type === 'rtcp-fb') {
57
+ description.feedbackSupport = description.feedbackSupport || [];
58
+ description.feedbackSupport.push(data);
59
+ } else if (type === 'fmtp') {
60
+ description.sdpFmtpLine = data;
61
+ }
62
+ });
63
+ return description;
64
+ }).filter(Boolean);
65
+ };
66
+
67
+ const codecsSdp = {
68
+ audio: constructDescriptions('audio', getDescriptors('audio')),
69
+ video: constructDescriptions('video', getDescriptors('video'))
70
+ };
71
+
72
+ // Set up for ICE candidate collection with timeout
73
+ const result = await new Promise<componentInterface>((resolveResult) => {
74
+ const timeout = setTimeout(() => {
75
+ connection.removeEventListener('icecandidate', onIceCandidate);
76
+ connection.close();
77
+ resolveResult({
78
+ supported: true,
79
+ codecsSdp,
80
+ extensions: extensions as string[],
81
+ timeout: true
82
+ });
83
+ }, 3000);
84
+
85
+ const onIceCandidate = (event: RTCPeerConnectionIceEvent) => {
86
+ const candidateObj = event.candidate;
87
+ if (!candidateObj || !candidateObj.candidate) return;
88
+
89
+ clearTimeout(timeout);
90
+ connection.removeEventListener('icecandidate', onIceCandidate);
91
+ connection.close();
92
+
93
+ resolveResult({
94
+ supported: true,
95
+ codecsSdp,
96
+ extensions: extensions as string[],
97
+ candidateType: candidateObj.type || ''
98
+ });
99
+ };
100
+
101
+ connection.addEventListener('icecandidate', onIceCandidate);
102
+ });
103
+
104
+ resolve({
105
+ hash: hash(JSON.stringify(result)),
106
+ });
107
+
108
+ } catch (error) {
109
+ connection.close();
110
+ resolve({
111
+ supported: true,
112
+ error: `WebRTC offer failed: ${(error as Error).message}`
113
+ });
114
+ }
115
+ };
116
+
117
+ processOffer();
118
+
119
+ } catch (error) {
120
+ resolve({
121
+ supported: false,
122
+ error: `WebRTC error: ${(error as Error).message}`
123
+ });
124
+ }
125
+ });
126
+ }
package/src/factory.ts CHANGED
@@ -20,6 +20,10 @@ import getScreen from "./components/screen";
20
20
  import getSystem from "./components/system";
21
21
  import getWebGL from "./components/webgl";
22
22
 
23
+ // Import experimental component functions
24
+ import getWebRTC from "./components/webrtc";
25
+ import getMathML from "./components/mathml";
26
+
23
27
  /**
24
28
  * @description key->function map of built-in components. Do not call the function here.
25
29
  */
@@ -37,6 +41,14 @@ export const tm_component_promises = {
37
41
  'webgl': getWebGL
38
42
  };
39
43
 
44
+ /**
45
+ * @description key->function map of experimental components. Only resolved during logging.
46
+ */
47
+ export const tm_experimental_component_promises = {
48
+ 'webrtc': getWebRTC,
49
+ 'mathml': getMathML
50
+ };
51
+
40
52
  // the component interface is the form of the JSON object the function's promise must return
41
53
  export interface componentInterface {
42
54
  [key: string]: string | string[] | number | boolean | componentInterface;
@@ -0,0 +1,130 @@
1
+ import { optionsInterface, API_ENDPOINT } from '../options';
2
+ import { componentInterface } from '../factory';
3
+ import { getVisitorId, setVisitorId } from '../utils/visitorId';
4
+ import { getVersion } from "../utils/version";
5
+ import { hash } from '../utils/hash';
6
+
7
+ // ===================== Types & Interfaces =====================
8
+
9
+ /**
10
+ * Info returned from the API (IP, classification, uniqueness, etc)
11
+ */
12
+ export interface infoInterface {
13
+ ip_address?: {
14
+ ip_address: string,
15
+ ip_identifier: string,
16
+ autonomous_system_number: number,
17
+ ip_version: 'v6' | 'v4',
18
+ },
19
+ classification?: {
20
+ tor: boolean,
21
+ vpn: boolean,
22
+ bot: boolean,
23
+ datacenter: boolean,
24
+ danger_level: number, // 5 is highest and should be blocked. 0 is no danger.
25
+ },
26
+ uniqueness?: {
27
+ score: number | string
28
+ },
29
+ timed_out?: boolean; // added for timeout handling
30
+ }
31
+
32
+ /**
33
+ * API response structure
34
+ */
35
+ interface apiResponse {
36
+ info?: infoInterface;
37
+ version?: string;
38
+ components?: componentInterface;
39
+ visitorId?: string;
40
+ }
41
+
42
+ // ===================== API Call Logic =====================
43
+
44
+ let currentApiPromise: Promise<apiResponse> | null = null;
45
+ let apiPromiseResult: apiResponse | null = null;
46
+
47
+ /**
48
+ * Calls the Thumbmark API with the given components, using caching and deduplication.
49
+ * Returns a promise for the API response or null on error.
50
+ */
51
+ export const getApiPromise = (
52
+ options: optionsInterface,
53
+ components: componentInterface
54
+ ): Promise<apiResponse | null> => {
55
+ // 1. If a result is already cached and caching is enabled, return it.
56
+ if (options.cache_api_call && apiPromiseResult) {
57
+ return Promise.resolve(apiPromiseResult);
58
+ }
59
+
60
+ // 2. If a request is already in flight, return that promise to prevent duplicate calls.
61
+ if (currentApiPromise) {
62
+ return currentApiPromise;
63
+ }
64
+
65
+ // 3. Otherwise, initiate a new API call with timeout.
66
+ const endpoint = `${API_ENDPOINT}/thumbmark`;
67
+ const visitorId = getVisitorId();
68
+ const requestBody: any = {
69
+ components,
70
+ options,
71
+ clientHash: hash(JSON.stringify(components)),
72
+ version: getVersion()
73
+ };
74
+ if (visitorId) {
75
+ requestBody.visitorId = visitorId;
76
+ }
77
+
78
+ const fetchPromise = fetch(endpoint, {
79
+ method: 'POST',
80
+ headers: {
81
+ 'x-api-key': options.api_key!,
82
+ 'Authorization': 'custom-authorized',
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ body: JSON.stringify(requestBody),
86
+ })
87
+ .then(response => {
88
+ // Handle HTTP errors that aren't network errors
89
+ if (!response.ok) {
90
+ if (response.status === 403) {
91
+ throw new Error('INVALID_API_KEY');
92
+ }
93
+ throw new Error(`HTTP error! status: ${response.status}`);
94
+ }
95
+ return response.json();
96
+ })
97
+ .then(data => {
98
+ // Handle visitor ID from server response
99
+ if (data.visitorId && data.visitorId !== visitorId) {
100
+ setVisitorId(data.visitorId);
101
+ }
102
+ apiPromiseResult = data; // Cache the successful result
103
+ currentApiPromise = null; // Clear the in-flight promise
104
+ return data;
105
+ })
106
+ .catch(error => {
107
+ console.error('Error fetching pro data', error);
108
+ currentApiPromise = null; // Also clear the in-flight promise on error
109
+ // For 403 errors, propagate the error instead of returning null
110
+ if (error.message === 'INVALID_API_KEY') {
111
+ throw error;
112
+ }
113
+ // Return null instead of a string to prevent downstream crashes
114
+ return null;
115
+ });
116
+
117
+ // Timeout logic
118
+ const timeoutMs = options.timeout || 5000;
119
+ const timeoutPromise = new Promise<apiResponse>((resolve) => {
120
+ setTimeout(() => {
121
+ resolve({
122
+ info: { timed_out: true },
123
+ version: getVersion(),
124
+ });
125
+ }, timeoutMs);
126
+ });
127
+
128
+ currentApiPromise = Promise.race([fetchPromise, timeoutPromise]);
129
+ return currentApiPromise;
130
+ };
@@ -47,7 +47,6 @@ export function filterThumbmarkData(
47
47
  if (!rules) continue;
48
48
 
49
49
  for (const rule of rules) {
50
- // FIX: Use the 'in' operator as a type guard to check if 'browsers' exists.
51
50
  // A rule applies to all browsers if the 'browsers' key is not present.
52
51
  const appliesToAllBrowsers = !('browsers' in rule);
53
52
 
@@ -4,22 +4,15 @@
4
4
  * This module handles component collection, API calls, uniqueness scoring, and data filtering
5
5
  * for the ThumbmarkJS browser fingerprinting library.
6
6
  *
7
- * Exports:
8
- * - getThumbmark
9
- * - getThumbmarkDataFromPromiseMap
10
- * - resolveClientComponents
11
- * - filterThumbmarkData
12
- *
13
- * Internal helpers and types are also defined here.
14
7
  */
15
8
 
16
- // ===================== Imports =====================
17
9
  import { defaultOptions, optionsInterface } from "../options";
18
10
  import {
19
11
  timeoutInstance,
20
12
  componentInterface,
21
13
  tm_component_promises,
22
14
  customComponents,
15
+ tm_experimental_component_promises,
23
16
  includeComponent as globalIncludeComponent
24
17
  } from "../factory";
25
18
  import { hash } from "../utils/hash";
@@ -27,43 +20,8 @@ import { raceAllPerformance } from "../utils/raceAll";
27
20
  import { getVersion } from "../utils/version";
28
21
  import { filterThumbmarkData } from './filterComponents'
29
22
  import { logThumbmarkData } from '../utils/log';
30
- import { API_ENDPOINT } from "../options";
31
- import { getVisitorId, setVisitorId } from "../utils/visitorId";
32
-
33
- // ===================== Types & Interfaces =====================
34
-
35
- /**
36
- * Info returned from the API (IP, classification, uniqueness, etc)
37
- */
38
- interface infoInterface {
39
- ip_address?: {
40
- ip_address: string,
41
- ip_identifier: string,
42
- autonomous_system_number: number,
43
- ip_version: 'v6' | 'v4',
44
- },
45
- classification?: {
46
- tor: boolean,
47
- vpn: boolean,
48
- bot: boolean,
49
- datacenter: boolean,
50
- danger_level: number, // 5 is highest and should be blocked. 0 is no danger.
51
- },
52
- uniqueness?: {
53
- score: number | string
54
- },
55
- timed_out?: boolean; // added for timeout handling
56
- }
23
+ import { getApiPromise, infoInterface } from "./api";
57
24
 
58
- /**
59
- * API response structure
60
- */
61
- interface apiResponse {
62
- info?: infoInterface;
63
- version?: string;
64
- components?: componentInterface;
65
- visitorId?: string;
66
- }
67
25
 
68
26
  /**
69
27
  * Final thumbmark response structure
@@ -74,101 +32,11 @@ interface thumbmarkResponse {
74
32
  version: string,
75
33
  thumbmark: string,
76
34
  visitorId?: string,
77
- /**
78
- * Only present if options.performance is true.
79
- */
80
35
  elapsed?: any;
36
+ error?: string;
37
+ experimental?: componentInterface;
81
38
  }
82
39
 
83
-
84
-
85
- // ===================== API Call Logic =====================
86
-
87
- let currentApiPromise: Promise<apiResponse> | null = null;
88
- let apiPromiseResult: apiResponse | null = null;
89
-
90
- /**
91
- * Calls the Thumbmark API with the given components, using caching and deduplication.
92
- * Returns a promise for the API response or null on error.
93
- */
94
- export const getApiPromise = (
95
- options: optionsInterface,
96
- components: componentInterface
97
- ): Promise<apiResponse | null> => {
98
- // 1. If a result is already cached and caching is enabled, return it.
99
- if (options.cache_api_call && apiPromiseResult) {
100
- return Promise.resolve(apiPromiseResult);
101
- }
102
-
103
- // 2. If a request is already in flight, return that promise to prevent duplicate calls.
104
- if (currentApiPromise) {
105
- return currentApiPromise;
106
- }
107
-
108
- // 3. Otherwise, initiate a new API call with timeout.
109
- const endpoint = `${API_ENDPOINT}/thumbmark`;
110
- const visitorId = getVisitorId();
111
- const requestBody: any = {
112
- components,
113
- options,
114
- clientHash: hash(JSON.stringify(components)),
115
- version: getVersion()
116
- };
117
- if (visitorId) {
118
- requestBody.visitorId = visitorId;
119
- }
120
-
121
- const fetchPromise = fetch(endpoint, {
122
- method: 'POST',
123
- headers: {
124
- 'x-api-key': options.api_key!,
125
- 'Authorization': 'custom-authorized',
126
- 'Content-Type': 'application/json',
127
- },
128
- body: JSON.stringify(requestBody),
129
- })
130
- .then(response => {
131
- // Handle HTTP errors that aren't network errors
132
- if (!response.ok) {
133
- throw new Error(`HTTP error! status: ${response.status}`);
134
- }
135
- return response.json();
136
- })
137
- .then(data => {
138
- // Handle visitor ID from server response
139
- if (data.visitorId && data.visitorId !== visitorId) {
140
- setVisitorId(data.visitorId);
141
- }
142
- apiPromiseResult = data; // Cache the successful result
143
- currentApiPromise = null; // Clear the in-flight promise
144
- return data;
145
- })
146
- .catch(error => {
147
- console.error('Error fetching pro data', error);
148
- currentApiPromise = null; // Also clear the in-flight promise on error
149
- // Return null instead of a string to prevent downstream crashes
150
- return null;
151
- });
152
-
153
- // Timeout logic
154
- const timeoutMs = options.timeout || 5000;
155
- const timeoutPromise = new Promise<apiResponse>((resolve) => {
156
- setTimeout(() => {
157
- resolve({
158
- info: { timed_out: true },
159
- version: getVersion(),
160
- });
161
- }, timeoutMs);
162
- });
163
-
164
- currentApiPromise = Promise.race([fetchPromise, timeoutPromise]);
165
- return currentApiPromise;
166
- };
167
-
168
- // ===================== Main Thumbmark Logic =====================
169
-
170
-
171
-
172
40
  /**
173
41
  * Main entry point: collects all components, optionally calls API, and returns thumbmark data.
174
42
  *
@@ -177,12 +45,41 @@ export const getApiPromise = (
177
45
  */
178
46
  export async function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse> {
179
47
  const _options = { ...defaultOptions, ...options };
48
+
49
+ // Early logging decision
50
+ const shouldLog = (!sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
51
+
180
52
  // Merge built-in and user-registered components
181
53
  const allComponents = { ...tm_component_promises, ...customComponents };
182
54
  const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options);
183
55
 
56
+ // Resolve experimental components only when logging
57
+ let experimentalComponents = {};
58
+ if (shouldLog || _options.experimental) {
59
+ const { resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
60
+ experimentalComponents = resolvedComponents;
61
+ }
62
+
184
63
  const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
185
- const apiResult = apiPromise ? await apiPromise : null;
64
+ let apiResult = null;
65
+
66
+ if (apiPromise) {
67
+ try {
68
+ apiResult = await apiPromise;
69
+ } catch (error) {
70
+ // Handle API key/quota errors
71
+ if (error instanceof Error && error.message === 'INVALID_API_KEY') {
72
+ return {
73
+ error: 'Invalid API key or quota exceeded',
74
+ components: {},
75
+ info: {},
76
+ version: getVersion(),
77
+ thumbmark: ''
78
+ };
79
+ }
80
+ throw error; // Re-throw other errors
81
+ }
82
+ }
186
83
 
187
84
  // Only add 'elapsed' if performance is true
188
85
  const maybeElapsed = _options.performance ? { elapsed } : {};
@@ -191,7 +88,10 @@ export async function getThumbmark(options?: optionsInterface): Promise<thumbmar
191
88
  const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
192
89
  const thumbmark = hash(JSON.stringify(components));
193
90
  const version = getVersion();
194
- logThumbmarkData(thumbmark, components, _options).catch(() => { /* do nothing */ });
91
+ // Only log to server when not in debug mode
92
+ if (shouldLog) {
93
+ logThumbmarkData(thumbmark, components, _options, experimentalComponents).catch(() => { /* do nothing */ });
94
+ }
195
95
 
196
96
  const result: thumbmarkResponse = {
197
97
  ...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
@@ -200,6 +100,7 @@ export async function getThumbmark(options?: optionsInterface): Promise<thumbmar
200
100
  info,
201
101
  version,
202
102
  ...maybeElapsed,
103
+ ...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
203
104
  };
204
105
 
205
106
  return result;
@@ -244,6 +145,4 @@ export async function resolveClientComponents(
244
145
  return { elapsed, resolvedComponents };
245
146
  }
246
147
 
247
-
248
-
249
148
  export { globalIncludeComponent as includeComponent };