@thumbmarkjs/thumbmarkjs
Version:
   • 5.71 kB
text/typescript
/**
* ThumbmarkJS: Main fingerprinting and API logic
*
* This module handles component collection, API calls, uniqueness scoring, and data filtering
* for the ThumbmarkJS browser fingerprinting library.
*
*/
import {defaultOptions, OptionsAfterDefaults, optionsInterface} from "../options";
import {
timeoutInstance,
componentInterface,
tm_component_promises,
customComponents,
tm_experimental_component_promises,
includeComponent as globalIncludeComponent
} from "../factory";
import { hash } from "../utils/hash";
import { raceAllPerformance } from "../utils/raceAll";
import { getVersion } from "../utils/version";
import { filterThumbmarkData } from './filterComponents'
import { logThumbmarkData } from '../utils/log';
import { getApiPromise, infoInterface } from "./api";
import { stableStringify } from "../utils/stableStringify";
/**
* Final thumbmark response structure
*/
interface thumbmarkResponse {
components: componentInterface,
info: { [key: string]: any },
version: string,
thumbmark: string,
visitorId?: string,
elapsed?: any;
error?: string;
experimental?: componentInterface;
}
/**
* Main entry point: collects all components, optionally calls API, and returns thumbmark data.
*
* @param options - Options for fingerprinting and API
* @returns thumbmarkResponse (elapsed is present only if options.performance is true)
*/
export async function getThumbmark(options?: optionsInterface): Promise<thumbmarkResponse> {
const _options = { ...defaultOptions, ...options } as OptionsAfterDefaults;
// Early logging decision
const shouldLog = (_options.logging && !sessionStorage.getItem("_tmjs_l") && Math.random() < 0.0001);
// Merge built-in and user-registered components
const allComponents = { ...tm_component_promises, ...customComponents };
const { elapsed, resolvedComponents: clientComponentsResult } = await resolveClientComponents(allComponents, _options);
// Resolve experimental components only when logging
let experimentalComponents = {};
let experimentalElapsed = {};
if (shouldLog || _options.experimental) {
const { elapsed: expElapsed, resolvedComponents } = await resolveClientComponents(tm_experimental_component_promises, _options);
experimentalComponents = resolvedComponents;
experimentalElapsed = expElapsed;
}
const apiPromise = _options.api_key ? getApiPromise(_options, clientComponentsResult) : null;
let apiResult = null;
if (apiPromise) {
try {
apiResult = await apiPromise;
} catch (error) {
// Handle API key/quota errors
if (error instanceof Error && error.message === 'INVALID_API_KEY') {
return {
error: 'Invalid API key or quota exceeded',
components: {},
info: {},
version: getVersion(),
thumbmark: ''
};
}
throw error; // Re-throw other errors
}
}
// Only add 'elapsed' if performance is true
const allElapsed = { ...elapsed, ...experimentalElapsed };
const maybeElapsed = _options.performance ? { elapsed: allElapsed } : {};
const apiComponents = filterThumbmarkData(apiResult?.components || {}, _options);
const components = { ...clientComponentsResult, ...apiComponents };
const info: infoInterface = apiResult?.info || { uniqueness: { score: 'api only' } };
// Use API thumbmark if available to ensure API/client sync, otherwise calculate locally
const thumbmark = apiResult?.thumbmark ?? hash(stableStringify(components));
const version = getVersion();
// Only log to server when not in debug mode
if (shouldLog) {
logThumbmarkData(thumbmark, components, _options, experimentalComponents).catch(() => { /* do nothing */ });
}
const result: thumbmarkResponse = {
...(apiResult?.visitorId && { visitorId: apiResult.visitorId }),
thumbmark,
components: components,
info,
version,
...maybeElapsed,
...(Object.keys(experimentalComponents).length > 0 && _options.experimental && { experimental: experimentalComponents }),
};
return result;
}
// ===================== Component Resolution & Performance =====================
/**
* Resolves and times all filtered component promises from a component function map.
*
* @param comps - Map of component functions
* @param options - Options for filtering and timing
* @returns Object with elapsed times and filtered resolved components
*/
export async function resolveClientComponents(
comps: { [key: string]: (options?: optionsInterface) => Promise<componentInterface | null> },
options?: optionsInterface
): Promise<{ elapsed: Record<string, number>, resolvedComponents: componentInterface }> {
const opts = { ...defaultOptions, ...options };
const filtered = Object.entries(comps)
.filter(([key]) => !opts?.exclude?.includes(key))
.filter(([key]) =>
opts?.include?.some(e => e.includes('.'))
? opts?.include?.some(e => e.startsWith(key))
: opts?.include?.length === 0 || opts?.include?.includes(key)
);
const keys = filtered.map(([key]) => key);
const promises = filtered.map(([_, fn]) => fn(options));
const resolvedValues = await raceAllPerformance(promises, opts?.timeout || 5000, timeoutInstance);
const elapsed: Record<string, number> = {};
const resolvedComponentsRaw: Record<string, componentInterface> = {};
resolvedValues.forEach((value, index) => {
if (value.value != null) {
resolvedComponentsRaw[keys[index]] = value.value;
elapsed[keys[index]] = value.elapsed ?? 0;
}
});
const resolvedComponents = filterThumbmarkData(resolvedComponentsRaw, opts);
return { elapsed, resolvedComponents };
}
export { globalIncludeComponent as includeComponent };