Skip to content

Commit 3b8b2a6

Browse files
committed
feat: add createAnalysisReport function for dense scoring reports
- Implemented createAnalysisReport to generate a concise report containing visitor ID, confidence, aggregate weights, risk verdicts, and raw component results. - Updated index files to export the new function. - Enhanced documentation to include details about the new report format. - Modified examples and tests to demonstrate the usage of createAnalysisReport. - Adjusted browser demo to display ID analysis alongside existing report formats.
1 parent fdc438a commit 3b8b2a6

17 files changed

Lines changed: 446 additions & 69 deletions

README.md

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@ FingerprintJS by BotBlocker is a browser fingerprinting and device intelligence
1010

1111
The runtime has no production dependencies, performs no network calls by default, and exposes privacy-aware collector policies, deterministic identity hashing, report-only risk signals, bot evidence, private-mode indicators, confidence scoring, storage state, and compact diagnostics.
1212

13+
## Core Advantages
14+
15+
- Stable visitor identity is separated from risk and diagnostic evidence. Volatile, bot, private-mode, tamper, storage, network, and capability signals can enrich reports without changing the default `visitorId`.
16+
- Backend verification is included through `@botblocker/fingerprintjs/server`: replay protection, client hash recomputation, server-only hashing, explainable backend reports, and pluggable network risk checks.
17+
- Tamper, bot, and private-mode evidence is returned as scored, explainable signals instead of a single opaque identifier.
18+
- Use-case presets provide practical defaults for privacy-first analytics, login risk, checkout risk, bot defense, and fraud defense.
19+
- The SDK is self-hostable, dependency-free at runtime, works through ESM, CommonJS, and a direct script tag, and performs no network calls unless the host application sends results to its own backend.
20+
- Output formats cover product integration, backend scoring, and debugging: compact report, dense ID analysis report, full raw report, and standalone JSON inspector.
21+
1322
## Install And Build
1423

1524
```bash
@@ -35,7 +44,7 @@ Each package entry supports ESM `import` and CommonJS `require`. Browser builds
3544
## ESM Usage
3645

3746
```js
38-
import { hashComponents, loadClient } from '@botblocker/fingerprintjs';
47+
import { createAnalysisReport, hashComponents, loadClient } from '@botblocker/fingerprintjs';
3948

4049
const client = await loadClient({
4150
namespace: 'my-product',
@@ -54,13 +63,15 @@ const result = await client.get({
5463
const bot = result.components.find((component) => component.id === 'browser.botDetection');
5564
const privacy = result.components.find((component) => component.id === 'browser.privacyMode');
5665
const recalculated = await hashComponents(result.components, { namespace: 'my-product' });
66+
const analysis = createAnalysisReport(result, { recalculatedHash: recalculated });
5767

5868
console.log({
5969
visitorId: result.visitorId,
6070
bot: bot?.value?.verdict,
6171
privateMode: privacy?.value?.verdict,
6272
confidence: result.confidence,
63-
hashMatches: recalculated.visitorId === result.visitorId
73+
hashMatches: recalculated.visitorId === result.visitorId,
74+
analysis
6475
});
6576
```
6677
@@ -190,15 +201,16 @@ Collection confidence and identity confidence are both exposed. `confidence.scor
190201
191202
## Reports And Demo
192203
193-
The browser demo in [examples/browser.html](examples/browser.html) renders two reports side by side and tracks repeated runs:
204+
The browser demo in [examples/browser.html](examples/browser.html) renders three reports side by side and tracks repeated runs:
194205
195-
- Compact report: concise identity, risk, quality, calculations, and every capability with status.
196-
- Full report: raw SDK result, recalculated hash, derived calculations, and every component value/error.
206+
- Compact report: concise identity, risk, quality, calculations, and every capability with status, role, hashability, weight, duration, value summary, and error state.
207+
- ID analysis report: shortest dense format for backend scoring. It contains `id`, request metadata, confidence, aggregate weights, hash checks, risk verdicts, and every component's role, status, weight, raw result, and error.
208+
- Full report: raw SDK result, recalculated hash, all-signals hash, derived calculations, stability data, explainable report, ID analysis report, and every component value/error.
197209
- Stability view: baseline visitor ID, current visitor ID, identity input count, report-only count, changed identity/report-only components, and recent run history.
198210
199-
Both reports include all collected capabilities and calculation data. Use the `extended` profile in the demo to exercise the full collector pack and confirm that report-only changes do not move the stable visitor ID.
211+
All demo outputs are generated from the same `IdentifyResult`. The compact and ID analysis formats include every collected capability; the full format additionally embeds the raw result and explainable report. Use the `extended` profile in the demo to exercise the full collector pack and confirm that report-only changes do not move the stable visitor ID.
200212
201-
The debug inspector in [examples/inspector.html](examples/inspector.html) accepts an `IdentifyResult` or full demo report JSON and explains identity components, report-only components, tamper, bot, and private-mode evidence.
213+
The debug inspector in [examples/inspector.html](examples/inspector.html) accepts an `IdentifyResult`, full demo report JSON, or ID analysis report JSON and explains identity components, report-only components, tamper, bot, and private-mode evidence.
202214
203215
## Verification
204216

dist/browser/fingerprintjs-botblocker.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ var FingerprintJSBotBlocker = (() => {
4343
VERSION: () => VERSION,
4444
canonicalStringify: () => canonicalStringify,
4545
componentsToDebugString: () => componentsToDebugString,
46+
createAnalysisReport: () => createAnalysisReport,
4647
createApiFeaturesCollector: () => createApiFeaturesCollector,
4748
createBotDetectionCollector: () => createBotDetectionCollector,
4849
createBrowserCollectorPack: () => createBrowserCollectorPack,
@@ -3257,6 +3258,29 @@ var FingerprintJSBotBlocker = (() => {
32573258
components
32583259
});
32593260
}
3261+
function createAnalysisReport(result, options = {}) {
3262+
assertResult2(result);
3263+
const identityComponents = new Set(result.meta && Array.isArray(result.meta.identityComponents) ? result.meta.identityComponents : []);
3264+
const components = Object.freeze(result.components.map((component) => analysisComponent(component, identityComponents)));
3265+
const risk = buildRiskSummary(result.components);
3266+
return Object.freeze({
3267+
id: result.visitorId || null,
3268+
requestId: result.requestId || null,
3269+
namespace: result.namespace || "default",
3270+
profile: result.meta && result.meta.profile ? result.meta.profile : null,
3271+
confidence: result.confidence || null,
3272+
weights: summarizeWeights(result.components, result.confidence || {}, identityComponents),
3273+
totals: Object.freeze({
3274+
total: components.length,
3275+
ok: countStatus(result.components, "ok"),
3276+
identity: components.filter((component) => component.role === "identity").length,
3277+
reportOnly: components.filter((component) => component.role === "report-only").length
3278+
}),
3279+
hash: buildHashSummary(result, options),
3280+
risk,
3281+
components
3282+
});
3283+
}
32603284
function explainComponent(component, identityComponents = [], options = {}) {
32613285
const identitySet = identityComponents instanceof Set ? identityComponents : new Set(identityComponents);
32623286
const role = identitySet.has(component.id) ? "identity" : "report-only";
@@ -3289,6 +3313,54 @@ var FingerprintJSBotBlocker = (() => {
32893313
}
32903314
return component.hashable === false ? "report_only_collector" : "excluded_by_identity_policy";
32913315
}
3316+
function analysisComponent(component, identityComponents) {
3317+
return Object.freeze({
3318+
id: component.id,
3319+
role: identityComponents.has(component.id) ? "identity" : "report-only",
3320+
status: component.status,
3321+
weight: component.weight,
3322+
category: component.category,
3323+
sensitivity: component.sensitivity,
3324+
mode: component.mode,
3325+
stability: component.stability,
3326+
hashable: component.hashable,
3327+
durationMs: component.durationMs,
3328+
result: component.status === "ok" ? component.value : null,
3329+
error: component.error || null
3330+
});
3331+
}
3332+
function summarizeWeights(components, confidence, identityComponents) {
3333+
const okComponents = components.filter((component) => component.status === "ok");
3334+
const identityOkComponents = okComponents.filter((component) => identityComponents.has(component.id));
3335+
const reportOnlyOkComponents = okComponents.filter((component) => !identityComponents.has(component.id));
3336+
return Object.freeze({
3337+
total: round2(sumWeights(components)),
3338+
ok: round2(sumWeights(okComponents)),
3339+
identity: round2(sumWeights(identityOkComponents)),
3340+
reportOnly: round2(sumWeights(reportOnlyOkComponents)),
3341+
collected: Number.isFinite(confidence.collectedWeight) ? confidence.collectedWeight : null,
3342+
possible: Number.isFinite(confidence.possibleWeight) ? confidence.possibleWeight : null,
3343+
qualityCollected: confidence.collectionQuality && Number.isFinite(confidence.collectionQuality.collectedWeight) ? confidence.collectionQuality.collectedWeight : null,
3344+
qualityPossible: confidence.collectionQuality && Number.isFinite(confidence.collectionQuality.possibleWeight) ? confidence.collectionQuality.possibleWeight : null
3345+
});
3346+
}
3347+
function buildHashSummary(result, options) {
3348+
const recalculated = options.recalculatedHash || null;
3349+
const allSignals = options.allSignalsHash || null;
3350+
return Object.freeze({
3351+
algorithm: result.meta && result.meta.hashAlgorithm ? result.meta.hashAlgorithm : null,
3352+
recalculatedVisitorId: recalculated ? recalculated.visitorId : null,
3353+
recalculatedMatches: recalculated ? recalculated.visitorId === result.visitorId : null,
3354+
allSignalsVisitorId: allSignals ? allSignals.visitorId : null,
3355+
allSignalsDiffers: allSignals ? allSignals.visitorId !== result.visitorId : null
3356+
});
3357+
}
3358+
function sumWeights(components) {
3359+
return components.reduce((total, component) => total + (Number.isFinite(component.weight) ? Number(component.weight) : 0), 0);
3360+
}
3361+
function round2(value) {
3362+
return Math.round(Number(value || 0) * 1e3) / 1e3;
3363+
}
32923364
function summarizeValue(value) {
32933365
if (value === null) {
32943366
return null;

dist/browser/fingerprintjs-botblocker.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.cjs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ __export(src_exports, {
3636
VERSION: () => VERSION,
3737
canonicalStringify: () => canonicalStringify,
3838
componentsToDebugString: () => componentsToDebugString,
39+
createAnalysisReport: () => createAnalysisReport,
3940
createApiFeaturesCollector: () => createApiFeaturesCollector,
4041
createBotDetectionCollector: () => createBotDetectionCollector,
4142
createBrowserCollectorPack: () => createBrowserCollectorPack,
@@ -3251,6 +3252,29 @@ function createExplainableReport(result, options = {}) {
32513252
components
32523253
});
32533254
}
3255+
function createAnalysisReport(result, options = {}) {
3256+
assertResult2(result);
3257+
const identityComponents = new Set(result.meta && Array.isArray(result.meta.identityComponents) ? result.meta.identityComponents : []);
3258+
const components = Object.freeze(result.components.map((component) => analysisComponent(component, identityComponents)));
3259+
const risk = buildRiskSummary(result.components);
3260+
return Object.freeze({
3261+
id: result.visitorId || null,
3262+
requestId: result.requestId || null,
3263+
namespace: result.namespace || "default",
3264+
profile: result.meta && result.meta.profile ? result.meta.profile : null,
3265+
confidence: result.confidence || null,
3266+
weights: summarizeWeights(result.components, result.confidence || {}, identityComponents),
3267+
totals: Object.freeze({
3268+
total: components.length,
3269+
ok: countStatus(result.components, "ok"),
3270+
identity: components.filter((component) => component.role === "identity").length,
3271+
reportOnly: components.filter((component) => component.role === "report-only").length
3272+
}),
3273+
hash: buildHashSummary(result, options),
3274+
risk,
3275+
components
3276+
});
3277+
}
32543278
function explainComponent(component, identityComponents = [], options = {}) {
32553279
const identitySet = identityComponents instanceof Set ? identityComponents : new Set(identityComponents);
32563280
const role = identitySet.has(component.id) ? "identity" : "report-only";
@@ -3283,6 +3307,54 @@ function explainReason(component, role) {
32833307
}
32843308
return component.hashable === false ? "report_only_collector" : "excluded_by_identity_policy";
32853309
}
3310+
function analysisComponent(component, identityComponents) {
3311+
return Object.freeze({
3312+
id: component.id,
3313+
role: identityComponents.has(component.id) ? "identity" : "report-only",
3314+
status: component.status,
3315+
weight: component.weight,
3316+
category: component.category,
3317+
sensitivity: component.sensitivity,
3318+
mode: component.mode,
3319+
stability: component.stability,
3320+
hashable: component.hashable,
3321+
durationMs: component.durationMs,
3322+
result: component.status === "ok" ? component.value : null,
3323+
error: component.error || null
3324+
});
3325+
}
3326+
function summarizeWeights(components, confidence, identityComponents) {
3327+
const okComponents = components.filter((component) => component.status === "ok");
3328+
const identityOkComponents = okComponents.filter((component) => identityComponents.has(component.id));
3329+
const reportOnlyOkComponents = okComponents.filter((component) => !identityComponents.has(component.id));
3330+
return Object.freeze({
3331+
total: round2(sumWeights(components)),
3332+
ok: round2(sumWeights(okComponents)),
3333+
identity: round2(sumWeights(identityOkComponents)),
3334+
reportOnly: round2(sumWeights(reportOnlyOkComponents)),
3335+
collected: Number.isFinite(confidence.collectedWeight) ? confidence.collectedWeight : null,
3336+
possible: Number.isFinite(confidence.possibleWeight) ? confidence.possibleWeight : null,
3337+
qualityCollected: confidence.collectionQuality && Number.isFinite(confidence.collectionQuality.collectedWeight) ? confidence.collectionQuality.collectedWeight : null,
3338+
qualityPossible: confidence.collectionQuality && Number.isFinite(confidence.collectionQuality.possibleWeight) ? confidence.collectionQuality.possibleWeight : null
3339+
});
3340+
}
3341+
function buildHashSummary(result, options) {
3342+
const recalculated = options.recalculatedHash || null;
3343+
const allSignals = options.allSignalsHash || null;
3344+
return Object.freeze({
3345+
algorithm: result.meta && result.meta.hashAlgorithm ? result.meta.hashAlgorithm : null,
3346+
recalculatedVisitorId: recalculated ? recalculated.visitorId : null,
3347+
recalculatedMatches: recalculated ? recalculated.visitorId === result.visitorId : null,
3348+
allSignalsVisitorId: allSignals ? allSignals.visitorId : null,
3349+
allSignalsDiffers: allSignals ? allSignals.visitorId !== result.visitorId : null
3350+
});
3351+
}
3352+
function sumWeights(components) {
3353+
return components.reduce((total, component) => total + (Number.isFinite(component.weight) ? Number(component.weight) : 0), 0);
3354+
}
3355+
function round2(value) {
3356+
return Math.round(Number(value || 0) * 1e3) / 1e3;
3357+
}
32863358
function summarizeValue(value) {
32873359
if (value === null) {
32883360
return null;
@@ -3322,6 +3394,7 @@ function countStatus(components, status) {
33223394
VERSION,
33233395
canonicalStringify,
33243396
componentsToDebugString,
3397+
createAnalysisReport,
33253398
createApiFeaturesCollector,
33263399
createBotDetectionCollector,
33273400
createBrowserCollectorPack,

dist/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ export function createStabilityMonitor(options?: { historyLimit?: number }): {
195195
snapshot(): Record<string, unknown>;
196196
};
197197
export function diffComponents(previousComponents?: ComponentResult[], currentComponents?: ComponentResult[], identityComponentIds?: string[]): Record<string, unknown>;
198+
export function createAnalysisReport(result: IdentifyResult, options?: { recalculatedHash?: HashComponentsResult; allSignalsHash?: HashComponentsResult }): Record<string, unknown>;
198199
export function createExplainableReport(result: IdentifyResult, options?: { generatedAt?: string; includeValues?: boolean }): Record<string, unknown>;
199200
export function explainComponent(component: ComponentResult, identityComponents?: string[] | Set<string>, options?: { includeValues?: boolean }): Record<string, unknown>;
200201
export function canonicalStringify(value: unknown): string;

dist/index.mjs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3190,6 +3190,29 @@ function createExplainableReport(result, options = {}) {
31903190
components
31913191
});
31923192
}
3193+
function createAnalysisReport(result, options = {}) {
3194+
assertResult2(result);
3195+
const identityComponents = new Set(result.meta && Array.isArray(result.meta.identityComponents) ? result.meta.identityComponents : []);
3196+
const components = Object.freeze(result.components.map((component) => analysisComponent(component, identityComponents)));
3197+
const risk = buildRiskSummary(result.components);
3198+
return Object.freeze({
3199+
id: result.visitorId || null,
3200+
requestId: result.requestId || null,
3201+
namespace: result.namespace || "default",
3202+
profile: result.meta && result.meta.profile ? result.meta.profile : null,
3203+
confidence: result.confidence || null,
3204+
weights: summarizeWeights(result.components, result.confidence || {}, identityComponents),
3205+
totals: Object.freeze({
3206+
total: components.length,
3207+
ok: countStatus(result.components, "ok"),
3208+
identity: components.filter((component) => component.role === "identity").length,
3209+
reportOnly: components.filter((component) => component.role === "report-only").length
3210+
}),
3211+
hash: buildHashSummary(result, options),
3212+
risk,
3213+
components
3214+
});
3215+
}
31933216
function explainComponent(component, identityComponents = [], options = {}) {
31943217
const identitySet = identityComponents instanceof Set ? identityComponents : new Set(identityComponents);
31953218
const role = identitySet.has(component.id) ? "identity" : "report-only";
@@ -3222,6 +3245,54 @@ function explainReason(component, role) {
32223245
}
32233246
return component.hashable === false ? "report_only_collector" : "excluded_by_identity_policy";
32243247
}
3248+
function analysisComponent(component, identityComponents) {
3249+
return Object.freeze({
3250+
id: component.id,
3251+
role: identityComponents.has(component.id) ? "identity" : "report-only",
3252+
status: component.status,
3253+
weight: component.weight,
3254+
category: component.category,
3255+
sensitivity: component.sensitivity,
3256+
mode: component.mode,
3257+
stability: component.stability,
3258+
hashable: component.hashable,
3259+
durationMs: component.durationMs,
3260+
result: component.status === "ok" ? component.value : null,
3261+
error: component.error || null
3262+
});
3263+
}
3264+
function summarizeWeights(components, confidence, identityComponents) {
3265+
const okComponents = components.filter((component) => component.status === "ok");
3266+
const identityOkComponents = okComponents.filter((component) => identityComponents.has(component.id));
3267+
const reportOnlyOkComponents = okComponents.filter((component) => !identityComponents.has(component.id));
3268+
return Object.freeze({
3269+
total: round2(sumWeights(components)),
3270+
ok: round2(sumWeights(okComponents)),
3271+
identity: round2(sumWeights(identityOkComponents)),
3272+
reportOnly: round2(sumWeights(reportOnlyOkComponents)),
3273+
collected: Number.isFinite(confidence.collectedWeight) ? confidence.collectedWeight : null,
3274+
possible: Number.isFinite(confidence.possibleWeight) ? confidence.possibleWeight : null,
3275+
qualityCollected: confidence.collectionQuality && Number.isFinite(confidence.collectionQuality.collectedWeight) ? confidence.collectionQuality.collectedWeight : null,
3276+
qualityPossible: confidence.collectionQuality && Number.isFinite(confidence.collectionQuality.possibleWeight) ? confidence.collectionQuality.possibleWeight : null
3277+
});
3278+
}
3279+
function buildHashSummary(result, options) {
3280+
const recalculated = options.recalculatedHash || null;
3281+
const allSignals = options.allSignalsHash || null;
3282+
return Object.freeze({
3283+
algorithm: result.meta && result.meta.hashAlgorithm ? result.meta.hashAlgorithm : null,
3284+
recalculatedVisitorId: recalculated ? recalculated.visitorId : null,
3285+
recalculatedMatches: recalculated ? recalculated.visitorId === result.visitorId : null,
3286+
allSignalsVisitorId: allSignals ? allSignals.visitorId : null,
3287+
allSignalsDiffers: allSignals ? allSignals.visitorId !== result.visitorId : null
3288+
});
3289+
}
3290+
function sumWeights(components) {
3291+
return components.reduce((total, component) => total + (Number.isFinite(component.weight) ? Number(component.weight) : 0), 0);
3292+
}
3293+
function round2(value) {
3294+
return Math.round(Number(value || 0) * 1e3) / 1e3;
3295+
}
32253296
function summarizeValue(value) {
32263297
if (value === null) {
32273298
return null;
@@ -3260,6 +3331,7 @@ export {
32603331
VERSION,
32613332
canonicalStringify,
32623333
componentsToDebugString,
3334+
createAnalysisReport,
32633335
createApiFeaturesCollector,
32643336
createBotDetectionCollector,
32653337
createBrowserCollectorPack,

0 commit comments

Comments
 (0)