@niksbanna/bot-detector 1.0.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.
- package/README.md +245 -0
- package/dist/bot-detector.cjs.js +2629 -0
- package/dist/bot-detector.cjs.js.map +7 -0
- package/dist/bot-detector.esm.js +2609 -0
- package/dist/bot-detector.esm.js.map +7 -0
- package/dist/bot-detector.iife.js +2631 -0
- package/dist/bot-detector.iife.js.map +7 -0
- package/dist/bot-detector.iife.min.js +1 -0
- package/package.json +57 -0
- package/src/core/BotDetector.js +284 -0
- package/src/core/ScoringEngine.js +134 -0
- package/src/core/Signal.js +181 -0
- package/src/core/VerdictEngine.js +132 -0
- package/src/index.js +273 -0
- package/src/signals/automation/PhantomJSSignal.js +137 -0
- package/src/signals/automation/PlaywrightSignal.js +129 -0
- package/src/signals/automation/PuppeteerSignal.js +117 -0
- package/src/signals/automation/SeleniumSignal.js +151 -0
- package/src/signals/automation/index.js +8 -0
- package/src/signals/behavior/InteractionTimingSignal.js +170 -0
- package/src/signals/behavior/KeyboardPatternSignal.js +235 -0
- package/src/signals/behavior/MouseMovementSignal.js +215 -0
- package/src/signals/behavior/ScrollBehaviorSignal.js +236 -0
- package/src/signals/behavior/index.js +8 -0
- package/src/signals/environment/HeadlessSignal.js +97 -0
- package/src/signals/environment/NavigatorAnomalySignal.js +117 -0
- package/src/signals/environment/PermissionsSignal.js +76 -0
- package/src/signals/environment/WebDriverSignal.js +58 -0
- package/src/signals/environment/index.js +8 -0
- package/src/signals/fingerprint/AudioContextSignal.js +158 -0
- package/src/signals/fingerprint/CanvasSignal.js +133 -0
- package/src/signals/fingerprint/PluginsSignal.js +106 -0
- package/src/signals/fingerprint/ScreenSignal.js +157 -0
- package/src/signals/fingerprint/WebGLSignal.js +146 -0
- package/src/signals/fingerprint/index.js +9 -0
- package/src/signals/timing/DOMContentTimingSignal.js +159 -0
- package/src/signals/timing/PageLoadSignal.js +165 -0
- package/src/signals/timing/index.js +6 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Detects WebGL rendering anomalies.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Signal } from '../../core/Signal.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detects WebGL anomalies and spoofed renderer information.
|
|
9
|
+
* Bots often have missing, disabled, or fake WebGL contexts.
|
|
10
|
+
*/
|
|
11
|
+
class WebGLSignal extends Signal {
|
|
12
|
+
static id = 'webgl';
|
|
13
|
+
static category = 'fingerprint';
|
|
14
|
+
static weight = 0.7;
|
|
15
|
+
static description = 'Detects WebGL rendering anomalies';
|
|
16
|
+
|
|
17
|
+
async detect() {
|
|
18
|
+
const anomalies = [];
|
|
19
|
+
let confidence = 0;
|
|
20
|
+
|
|
21
|
+
// Try to get WebGL context
|
|
22
|
+
const canvas = document.createElement('canvas');
|
|
23
|
+
let gl = null;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
27
|
+
} catch (e) {
|
|
28
|
+
anomalies.push('webgl-error');
|
|
29
|
+
confidence = Math.max(confidence, 0.5);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!gl) {
|
|
33
|
+
// WebGL not available - could be disabled or blocked
|
|
34
|
+
anomalies.push('webgl-unavailable');
|
|
35
|
+
confidence = Math.max(confidence, 0.4);
|
|
36
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get renderer info
|
|
40
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
41
|
+
let vendor = '';
|
|
42
|
+
let renderer = '';
|
|
43
|
+
|
|
44
|
+
if (debugInfo) {
|
|
45
|
+
vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || '';
|
|
46
|
+
renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for empty renderer info (common in headless)
|
|
50
|
+
if (!vendor && !renderer) {
|
|
51
|
+
anomalies.push('no-webgl-renderer-info');
|
|
52
|
+
confidence = Math.max(confidence, 0.6);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check for known headless/virtual renderer strings
|
|
56
|
+
const suspiciousRenderers = [
|
|
57
|
+
'swiftshader',
|
|
58
|
+
'llvmpipe',
|
|
59
|
+
'software',
|
|
60
|
+
'mesa',
|
|
61
|
+
'google swiftshader',
|
|
62
|
+
'vmware',
|
|
63
|
+
'virtualbox',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const rendererLower = renderer.toLowerCase();
|
|
67
|
+
for (const sus of suspiciousRenderers) {
|
|
68
|
+
if (rendererLower.includes(sus)) {
|
|
69
|
+
anomalies.push(`suspicious-renderer-${sus.replace(/\s+/g, '-')}`);
|
|
70
|
+
confidence = Math.max(confidence, 0.7);
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check for mismatched vendor/renderer
|
|
76
|
+
if (vendor && renderer) {
|
|
77
|
+
// NVIDIA renderer should have NVIDIA vendor
|
|
78
|
+
if (rendererLower.includes('nvidia') && !vendor.toLowerCase().includes('nvidia')) {
|
|
79
|
+
anomalies.push('vendor-renderer-mismatch');
|
|
80
|
+
confidence = Math.max(confidence, 0.6);
|
|
81
|
+
}
|
|
82
|
+
// AMD renderer should have AMD/ATI vendor
|
|
83
|
+
if ((rendererLower.includes('amd') || rendererLower.includes('radeon')) &&
|
|
84
|
+
!vendor.toLowerCase().includes('amd') && !vendor.toLowerCase().includes('ati')) {
|
|
85
|
+
anomalies.push('vendor-renderer-mismatch');
|
|
86
|
+
confidence = Math.max(confidence, 0.6);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for supported extensions
|
|
91
|
+
const extensions = gl.getSupportedExtensions() || [];
|
|
92
|
+
|
|
93
|
+
// Suspiciously few extensions
|
|
94
|
+
if (extensions.length < 5) {
|
|
95
|
+
anomalies.push('few-webgl-extensions');
|
|
96
|
+
confidence = Math.max(confidence, 0.4);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for WebGL parameter consistency
|
|
100
|
+
const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
101
|
+
const maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
|
|
102
|
+
|
|
103
|
+
// Unrealistic values
|
|
104
|
+
if (maxTextureSize < 1024 || maxTextureSize > 65536) {
|
|
105
|
+
anomalies.push('unrealistic-max-texture');
|
|
106
|
+
confidence = Math.max(confidence, 0.5);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check if WebGL rendering actually works
|
|
110
|
+
try {
|
|
111
|
+
// Simple render test
|
|
112
|
+
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
|
113
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
114
|
+
|
|
115
|
+
const pixels = new Uint8Array(4);
|
|
116
|
+
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
|
117
|
+
|
|
118
|
+
// If clear didn't work, something's wrong
|
|
119
|
+
if (pixels[3] !== 255) {
|
|
120
|
+
anomalies.push('webgl-render-failure');
|
|
121
|
+
confidence = Math.max(confidence, 0.6);
|
|
122
|
+
}
|
|
123
|
+
} catch (e) {
|
|
124
|
+
anomalies.push('webgl-render-error');
|
|
125
|
+
confidence = Math.max(confidence, 0.5);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Clean up
|
|
129
|
+
const loseContext = gl.getExtension('WEBGL_lose_context');
|
|
130
|
+
if (loseContext) {
|
|
131
|
+
loseContext.loseContext();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const triggered = anomalies.length > 0;
|
|
135
|
+
|
|
136
|
+
return this.createResult(triggered, {
|
|
137
|
+
anomalies,
|
|
138
|
+
vendor,
|
|
139
|
+
renderer,
|
|
140
|
+
extensionCount: extensions.length,
|
|
141
|
+
maxTextureSize,
|
|
142
|
+
}, confidence);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export { WebGLSignal };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Fingerprint signals index.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { PluginsSignal } from './PluginsSignal.js';
|
|
6
|
+
export { WebGLSignal } from './WebGLSignal.js';
|
|
7
|
+
export { CanvasSignal } from './CanvasSignal.js';
|
|
8
|
+
export { AudioContextSignal } from './AudioContextSignal.js';
|
|
9
|
+
export { ScreenSignal } from './ScreenSignal.js';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Analyzes DOM content loaded event timing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Signal } from '../../core/Signal.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Analyzes timing around DOMContentLoaded event.
|
|
9
|
+
* Bots may have unusual patterns in when/how DOM is processed.
|
|
10
|
+
*/
|
|
11
|
+
class DOMContentTimingSignal extends Signal {
|
|
12
|
+
static id = 'dom-content-timing';
|
|
13
|
+
static category = 'timing';
|
|
14
|
+
static weight = 0.4;
|
|
15
|
+
static description = 'Analyzes DOM content loaded timing patterns';
|
|
16
|
+
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
super(options);
|
|
19
|
+
this._domContentLoadedTime = null;
|
|
20
|
+
this._documentReadyState = document.readyState;
|
|
21
|
+
this._captureTime = performance.now();
|
|
22
|
+
|
|
23
|
+
// Capture DOMContentLoaded time if not already loaded
|
|
24
|
+
if (document.readyState === 'loading') {
|
|
25
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
26
|
+
this._domContentLoadedTime = performance.now();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async detect() {
|
|
32
|
+
const anomalies = [];
|
|
33
|
+
let confidence = 0;
|
|
34
|
+
|
|
35
|
+
// Get timing information
|
|
36
|
+
const now = performance.now();
|
|
37
|
+
const readyState = document.readyState;
|
|
38
|
+
|
|
39
|
+
// Check resource timing
|
|
40
|
+
let resourceCount = 0;
|
|
41
|
+
let totalResourceTime = 0;
|
|
42
|
+
let externalScriptCount = 0;
|
|
43
|
+
|
|
44
|
+
if (performance.getEntriesByType) {
|
|
45
|
+
const resources = performance.getEntriesByType('resource');
|
|
46
|
+
resourceCount = resources.length;
|
|
47
|
+
|
|
48
|
+
for (const resource of resources) {
|
|
49
|
+
totalResourceTime += resource.duration;
|
|
50
|
+
if (resource.initiatorType === 'script' &&
|
|
51
|
+
resource.name.startsWith('http')) {
|
|
52
|
+
externalScriptCount++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check for very few resources (headless often loads minimal)
|
|
58
|
+
if (resourceCount === 0 && readyState === 'complete') {
|
|
59
|
+
anomalies.push('no-resources-loaded');
|
|
60
|
+
confidence = Math.max(confidence, 0.4);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for suspiciously fast DOM ready without resources
|
|
64
|
+
if (this._domContentLoadedTime && this._domContentLoadedTime < 50 && resourceCount === 0) {
|
|
65
|
+
anomalies.push('instant-ready-no-resources');
|
|
66
|
+
confidence = Math.max(confidence, 0.6);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check document.hidden state at load
|
|
70
|
+
// Bots often run in hidden/background state
|
|
71
|
+
if (document.hidden && this._documentReadyState === 'loading') {
|
|
72
|
+
anomalies.push('hidden-at-load');
|
|
73
|
+
confidence = Math.max(confidence, 0.3);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check for visibility API
|
|
77
|
+
if (typeof document.visibilityState === 'undefined') {
|
|
78
|
+
anomalies.push('no-visibility-api');
|
|
79
|
+
confidence = Math.max(confidence, 0.4);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check DOM manipulation timing
|
|
83
|
+
try {
|
|
84
|
+
const startMutation = performance.now();
|
|
85
|
+
const testDiv = document.createElement('div');
|
|
86
|
+
testDiv.id = '__bot_detection_test__';
|
|
87
|
+
document.body.appendChild(testDiv);
|
|
88
|
+
const afterAppend = performance.now();
|
|
89
|
+
document.body.removeChild(testDiv);
|
|
90
|
+
const afterRemove = performance.now();
|
|
91
|
+
|
|
92
|
+
const appendTime = afterAppend - startMutation;
|
|
93
|
+
const removeTime = afterRemove - afterAppend;
|
|
94
|
+
|
|
95
|
+
// Instant DOM operations (< 0.01ms) may indicate mocked DOM
|
|
96
|
+
if (appendTime === 0 && removeTime === 0) {
|
|
97
|
+
anomalies.push('instant-dom-operations');
|
|
98
|
+
confidence = Math.max(confidence, 0.5);
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// If body doesn't exist yet, that's unusual at detection time
|
|
102
|
+
if (!document.body) {
|
|
103
|
+
anomalies.push('no-document-body');
|
|
104
|
+
confidence = Math.max(confidence, 0.4);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for MutationObserver availability (should exist in modern browsers)
|
|
109
|
+
if (typeof MutationObserver === 'undefined') {
|
|
110
|
+
anomalies.push('no-mutation-observer');
|
|
111
|
+
confidence = Math.max(confidence, 0.5);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check for requestAnimationFrame availability
|
|
115
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
116
|
+
anomalies.push('no-request-animation-frame');
|
|
117
|
+
confidence = Math.max(confidence, 0.5);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check timing of first paint if available
|
|
121
|
+
if (performance.getEntriesByType) {
|
|
122
|
+
const paintEntries = performance.getEntriesByType('paint');
|
|
123
|
+
const firstPaint = paintEntries.find(e => e.name === 'first-paint');
|
|
124
|
+
|
|
125
|
+
if (!firstPaint && readyState === 'complete' && now > 1000) {
|
|
126
|
+
anomalies.push('no-first-paint');
|
|
127
|
+
confidence = Math.max(confidence, 0.4);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for first contentful paint
|
|
131
|
+
const fcp = paintEntries.find(e => e.name === 'first-contentful-paint');
|
|
132
|
+
if (!fcp && readyState === 'complete' && now > 1000) {
|
|
133
|
+
anomalies.push('no-first-contentful-paint');
|
|
134
|
+
confidence = Math.max(confidence, 0.4);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check intersection observer
|
|
139
|
+
if (typeof IntersectionObserver === 'undefined') {
|
|
140
|
+
anomalies.push('no-intersection-observer');
|
|
141
|
+
confidence = Math.max(confidence, 0.4);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const triggered = anomalies.length > 0;
|
|
145
|
+
|
|
146
|
+
return this.createResult(triggered, {
|
|
147
|
+
anomalies,
|
|
148
|
+
metrics: {
|
|
149
|
+
readyState,
|
|
150
|
+
resourceCount,
|
|
151
|
+
externalScriptCount,
|
|
152
|
+
domContentLoadedTime: this._domContentLoadedTime,
|
|
153
|
+
documentHidden: document.hidden,
|
|
154
|
+
},
|
|
155
|
+
}, confidence);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export { DOMContentTimingSignal };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Detects suspicious page load timing patterns.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Signal } from '../../core/Signal.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Analyzes page load timing for automation indicators.
|
|
9
|
+
* Bots often have unusual or suspiciously fast load patterns.
|
|
10
|
+
*/
|
|
11
|
+
class PageLoadSignal extends Signal {
|
|
12
|
+
static id = 'page-load';
|
|
13
|
+
static category = 'timing';
|
|
14
|
+
static weight = 0.5;
|
|
15
|
+
static description = 'Detects suspicious page load timing';
|
|
16
|
+
|
|
17
|
+
async detect() {
|
|
18
|
+
const anomalies = [];
|
|
19
|
+
let confidence = 0;
|
|
20
|
+
|
|
21
|
+
// Check if Performance API is available
|
|
22
|
+
if (!window.performance || !performance.timing) {
|
|
23
|
+
// Try Navigation Timing API Level 2
|
|
24
|
+
if (performance.getEntriesByType) {
|
|
25
|
+
const navEntries = performance.getEntriesByType('navigation');
|
|
26
|
+
if (navEntries.length > 0) {
|
|
27
|
+
return this._analyzeNavigationTiming(navEntries[0]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
anomalies.push('no-performance-api');
|
|
32
|
+
confidence = Math.max(confidence, 0.3);
|
|
33
|
+
return this.createResult(true, { anomalies }, confidence);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const timing = performance.timing;
|
|
37
|
+
|
|
38
|
+
// Calculate key timings
|
|
39
|
+
const navigationStart = timing.navigationStart;
|
|
40
|
+
const domContentLoaded = timing.domContentLoadedEventEnd - navigationStart;
|
|
41
|
+
const domComplete = timing.domComplete - navigationStart;
|
|
42
|
+
const loadComplete = timing.loadEventEnd - navigationStart;
|
|
43
|
+
const dnsLookup = timing.domainLookupEnd - timing.domainLookupStart;
|
|
44
|
+
const tcpConnection = timing.connectEnd - timing.connectStart;
|
|
45
|
+
const serverResponse = timing.responseEnd - timing.requestStart;
|
|
46
|
+
const domProcessing = timing.domComplete - timing.domLoading;
|
|
47
|
+
|
|
48
|
+
// Check for impossibly fast load times
|
|
49
|
+
if (domContentLoaded > 0 && domContentLoaded < 10) {
|
|
50
|
+
anomalies.push('instant-dom-content-loaded');
|
|
51
|
+
confidence = Math.max(confidence, 0.7);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check for zero DNS lookup (could indicate local file or caching, but suspicious in combination)
|
|
55
|
+
if (dnsLookup === 0 && tcpConnection === 0 && serverResponse < 5) {
|
|
56
|
+
anomalies.push('zero-network-timing');
|
|
57
|
+
confidence = Math.max(confidence, 0.4);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check for negative timings (timestamp manipulation)
|
|
61
|
+
if (domContentLoaded < 0 || domComplete < 0 || loadComplete < 0) {
|
|
62
|
+
anomalies.push('negative-timing');
|
|
63
|
+
confidence = Math.max(confidence, 0.8);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for unrealistic timing order
|
|
67
|
+
if (timing.domContentLoadedEventEnd > 0 && timing.loadEventEnd > 0) {
|
|
68
|
+
if (timing.domContentLoadedEventEnd > timing.loadEventEnd) {
|
|
69
|
+
anomalies.push('timing-order-violation');
|
|
70
|
+
confidence = Math.max(confidence, 0.7);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check for very long processing times (could indicate headless waiting)
|
|
75
|
+
if (domProcessing > 30000) { // 30 seconds
|
|
76
|
+
anomalies.push('excessive-dom-processing');
|
|
77
|
+
confidence = Math.max(confidence, 0.3);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check for script injection timing pattern
|
|
81
|
+
// Bots often inject scripts immediately after load
|
|
82
|
+
const scriptsLoadedTime = timing.domContentLoadedEventStart - timing.responseEnd;
|
|
83
|
+
if (scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
|
|
84
|
+
anomalies.push('instant-script-execution');
|
|
85
|
+
confidence = Math.max(confidence, 0.4);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for performance.now() manipulation
|
|
89
|
+
const perfNow1 = performance.now();
|
|
90
|
+
const perfNow2 = performance.now();
|
|
91
|
+
|
|
92
|
+
// If two consecutive calls return the same value (shouldn't happen)
|
|
93
|
+
if (perfNow1 === perfNow2 && perfNow1 > 0) {
|
|
94
|
+
anomalies.push('frozen-performance-now');
|
|
95
|
+
confidence = Math.max(confidence, 0.6);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check for Date.now() vs performance.now() consistency
|
|
99
|
+
const dateNow1 = Date.now();
|
|
100
|
+
const perfNow3 = performance.now();
|
|
101
|
+
const dateNow2 = Date.now();
|
|
102
|
+
|
|
103
|
+
// If they're wildly inconsistent
|
|
104
|
+
if (Math.abs((dateNow2 - dateNow1) - (performance.now() - perfNow3)) > 100) {
|
|
105
|
+
anomalies.push('timing-inconsistency');
|
|
106
|
+
confidence = Math.max(confidence, 0.5);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const triggered = anomalies.length > 0;
|
|
110
|
+
|
|
111
|
+
return this.createResult(triggered, {
|
|
112
|
+
anomalies,
|
|
113
|
+
timings: {
|
|
114
|
+
domContentLoaded,
|
|
115
|
+
domComplete,
|
|
116
|
+
loadComplete,
|
|
117
|
+
dnsLookup,
|
|
118
|
+
tcpConnection,
|
|
119
|
+
serverResponse,
|
|
120
|
+
domProcessing,
|
|
121
|
+
},
|
|
122
|
+
}, confidence);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Analyze Navigation Timing Level 2 API data.
|
|
127
|
+
* @param {PerformanceNavigationTiming} entry - Navigation timing entry
|
|
128
|
+
* @returns {SignalResult}
|
|
129
|
+
*/
|
|
130
|
+
_analyzeNavigationTiming(entry) {
|
|
131
|
+
const anomalies = [];
|
|
132
|
+
let confidence = 0;
|
|
133
|
+
|
|
134
|
+
const domContentLoaded = entry.domContentLoadedEventEnd;
|
|
135
|
+
const loadComplete = entry.loadEventEnd;
|
|
136
|
+
const dnsLookup = entry.domainLookupEnd - entry.domainLookupStart;
|
|
137
|
+
const serverResponse = entry.responseEnd - entry.requestStart;
|
|
138
|
+
|
|
139
|
+
// Check for impossibly fast load
|
|
140
|
+
if (domContentLoaded > 0 && domContentLoaded < 10) {
|
|
141
|
+
anomalies.push('instant-dom-content-loaded');
|
|
142
|
+
confidence = Math.max(confidence, 0.7);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check for zero timings
|
|
146
|
+
if (dnsLookup === 0 && serverResponse === 0) {
|
|
147
|
+
anomalies.push('zero-network-timing');
|
|
148
|
+
confidence = Math.max(confidence, 0.4);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const triggered = anomalies.length > 0;
|
|
152
|
+
|
|
153
|
+
return this.createResult(triggered, {
|
|
154
|
+
anomalies,
|
|
155
|
+
timings: {
|
|
156
|
+
domContentLoaded,
|
|
157
|
+
loadComplete,
|
|
158
|
+
dnsLookup,
|
|
159
|
+
serverResponse,
|
|
160
|
+
},
|
|
161
|
+
}, confidence);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { PageLoadSignal };
|