@djangocfg/monitor 2.1.236 → 2.1.238
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/dist/client.cjs +90 -14
- package/dist/client.cjs.map +1 -1
- package/dist/client.mjs +90 -14
- package/dist/client.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client/capture/console.ts +17 -0
- package/src/client/capture/network.ts +36 -13
- package/src/client/capture/validation.ts +19 -0
- package/src/client/store/index.ts +31 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/monitor",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.238",
|
|
4
4
|
"description": "Browser error and event monitoring SDK for django-cfg backends. Captures JS errors, network failures, console logs, and performance metrics.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"django",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
86
|
+
"@djangocfg/typescript-config": "^2.1.238",
|
|
87
87
|
"p-retry": "^6.2.0",
|
|
88
88
|
"@types/node": "^24.7.2",
|
|
89
89
|
"@types/react": "^19.1.0",
|
|
@@ -18,6 +18,22 @@ const typeMap: Record<ConsoleLevel, FrontendEventIngestRequestEventType> = {
|
|
|
18
18
|
|
|
19
19
|
const ARG_MAX = 500
|
|
20
20
|
|
|
21
|
+
// Dedup: skip re-sending the same console message within TTL
|
|
22
|
+
const CONSOLE_DEDUP_TTL = 5 * 60 * 1000 // 5 minutes
|
|
23
|
+
const recentConsoleFingerprints = new Map<string, number>()
|
|
24
|
+
|
|
25
|
+
function isRecentConsole(fingerprint: string): boolean {
|
|
26
|
+
const now = Date.now()
|
|
27
|
+
const last = recentConsoleFingerprints.get(fingerprint)
|
|
28
|
+
if (last !== undefined && now - last < CONSOLE_DEDUP_TTL) return true
|
|
29
|
+
recentConsoleFingerprints.set(fingerprint, now)
|
|
30
|
+
if (recentConsoleFingerprints.size > 100) {
|
|
31
|
+
const oldest = [...recentConsoleFingerprints.entries()].sort((a, b) => a[1] - b[1])[0]
|
|
32
|
+
recentConsoleFingerprints.delete(oldest[0])
|
|
33
|
+
}
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
function stringifyArg(a: unknown): string {
|
|
22
38
|
let s: string
|
|
23
39
|
if (typeof a === 'string') s = a
|
|
@@ -36,6 +52,7 @@ async function captureConsoleEvent(level: ConsoleLevel, args: unknown[]) {
|
|
|
36
52
|
if (MONITOR_INGEST_PATTERN.test(message)) return
|
|
37
53
|
const url = typeof window !== 'undefined' ? window.location.href : ''
|
|
38
54
|
const fingerprint = await computeFingerprint(message, '', url)
|
|
55
|
+
if (isRecentConsole(fingerprint)) return
|
|
39
56
|
const { config } = monitorStore.getState()
|
|
40
57
|
monitorStore.getState().push({
|
|
41
58
|
event_type: typeMap[level],
|
|
@@ -2,6 +2,23 @@ import { getSessionId } from './session'
|
|
|
2
2
|
import { monitorStore } from '../store'
|
|
3
3
|
import { EventType, EventLevel } from '../../_api'
|
|
4
4
|
|
|
5
|
+
// Dedup: skip re-sending the same network error within TTL
|
|
6
|
+
const NETWORK_DEDUP_TTL = 5_000 // 5 seconds
|
|
7
|
+
const recentNetworkErrors = new Map<string, number>()
|
|
8
|
+
|
|
9
|
+
function isRecentNetwork(key: string): boolean {
|
|
10
|
+
const now = Date.now()
|
|
11
|
+
const last = recentNetworkErrors.get(key)
|
|
12
|
+
if (last !== undefined && now - last < NETWORK_DEDUP_TTL) return true
|
|
13
|
+
recentNetworkErrors.set(key, now)
|
|
14
|
+
if (recentNetworkErrors.size > 100) {
|
|
15
|
+
for (const [k, ts] of recentNetworkErrors) {
|
|
16
|
+
if (now - ts > NETWORK_DEDUP_TTL) recentNetworkErrors.delete(k)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
export async function monitoredFetch(
|
|
6
23
|
input: RequestInfo | URL,
|
|
7
24
|
init?: RequestInit,
|
|
@@ -12,6 +29,8 @@ export async function monitoredFetch(
|
|
|
12
29
|
try {
|
|
13
30
|
const response = await fetch(input, init)
|
|
14
31
|
if (!response.ok) {
|
|
32
|
+
const netKey = `${response.status}:${method}:${url}`
|
|
33
|
+
if (isRecentNetwork(netKey)) return response
|
|
15
34
|
const { config } = monitorStore.getState()
|
|
16
35
|
monitorStore.getState().push({
|
|
17
36
|
event_type: EventType.NETWORK_ERROR,
|
|
@@ -29,19 +48,23 @@ export async function monitoredFetch(
|
|
|
29
48
|
}
|
|
30
49
|
return response
|
|
31
50
|
} catch (err) {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
const errMsg = err instanceof Error ? err.message : 'network-error'
|
|
52
|
+
const netKey = `err:${errMsg.slice(0, 50)}:${method}:${url}`
|
|
53
|
+
if (!isRecentNetwork(netKey)) {
|
|
54
|
+
const { config } = monitorStore.getState()
|
|
55
|
+
monitorStore.getState().push({
|
|
56
|
+
event_type: EventType.NETWORK_ERROR,
|
|
57
|
+
level: EventLevel.ERROR,
|
|
58
|
+
message: err instanceof Error ? err.message : `Network error — ${method} ${url}`,
|
|
59
|
+
url: typeof window !== 'undefined' ? window.location.href : '',
|
|
60
|
+
http_method: method,
|
|
61
|
+
http_url: url,
|
|
62
|
+
session_id: getSessionId(),
|
|
63
|
+
user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
64
|
+
project_name: config.project,
|
|
65
|
+
environment: config.environment,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
45
68
|
throw err
|
|
46
69
|
}
|
|
47
70
|
}
|
|
@@ -2,6 +2,23 @@ import { getSessionId } from './session'
|
|
|
2
2
|
import { monitorStore } from '../store'
|
|
3
3
|
import { EventType, EventLevel } from '../../_api'
|
|
4
4
|
|
|
5
|
+
// Dedup: skip re-sending the same validation error within TTL
|
|
6
|
+
const VALIDATION_DEDUP_TTL = 10_000 // 10 seconds
|
|
7
|
+
const recentValidations = new Map<string, number>()
|
|
8
|
+
|
|
9
|
+
function isRecentValidation(key: string): boolean {
|
|
10
|
+
const now = Date.now()
|
|
11
|
+
const last = recentValidations.get(key)
|
|
12
|
+
if (last !== undefined && now - last < VALIDATION_DEDUP_TTL) return true
|
|
13
|
+
recentValidations.set(key, now)
|
|
14
|
+
if (recentValidations.size > 50) {
|
|
15
|
+
for (const [k, ts] of recentValidations) {
|
|
16
|
+
if (now - ts > VALIDATION_DEDUP_TTL) recentValidations.delete(k)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
interface ValidationErrorDetail {
|
|
6
23
|
operation: string
|
|
7
24
|
path: string
|
|
@@ -16,6 +33,8 @@ export function installValidationCapture(): () => void {
|
|
|
16
33
|
if (!(event instanceof CustomEvent)) return
|
|
17
34
|
try {
|
|
18
35
|
const detail = event.detail as ValidationErrorDetail
|
|
36
|
+
const dedupeKey = `${detail.operation}:${detail.path}:${detail.method}`
|
|
37
|
+
if (isRecentValidation(dedupeKey)) return
|
|
19
38
|
const { config } = monitorStore.getState()
|
|
20
39
|
const rawMsg = `Zod validation error in ${detail.operation}: ${detail.error?.message ?? 'unknown'}`
|
|
21
40
|
monitorStore.getState().push({
|
|
@@ -7,6 +7,33 @@ import { MONITOR_VERSION } from '../utils/env'
|
|
|
7
7
|
const CIRCUIT_BREAKER_THRESHOLD = 3
|
|
8
8
|
const CIRCUIT_BREAKER_COOLDOWN_MS = 60_000 // 1 minute
|
|
9
9
|
|
|
10
|
+
// Global deduplication: skip pushing identical events within this window.
|
|
11
|
+
// Protects against multiple capture sources (js-errors, console, ErrorTrackingProvider)
|
|
12
|
+
// all firing for the same underlying error.
|
|
13
|
+
const STORE_DEDUP_TTL = 5_000 // 5 seconds
|
|
14
|
+
const STORE_DEDUP_MAX = 200
|
|
15
|
+
const _recentPushKeys = new Map<string, number>()
|
|
16
|
+
|
|
17
|
+
function _pushDedupeKey(event: MonitorEvent): string {
|
|
18
|
+
// Build a key from event_type + level + message (first 100 chars) + url
|
|
19
|
+
const msg = (event.message ?? '').slice(0, 100)
|
|
20
|
+
return `${event.event_type}:${event.level}:${msg}:${event.http_url ?? event.url ?? ''}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _isRecentPush(key: string): boolean {
|
|
24
|
+
const now = Date.now()
|
|
25
|
+
const last = _recentPushKeys.get(key)
|
|
26
|
+
if (last !== undefined && now - last < STORE_DEDUP_TTL) return true
|
|
27
|
+
_recentPushKeys.set(key, now)
|
|
28
|
+
// Evict old entries when map grows too large
|
|
29
|
+
if (_recentPushKeys.size > STORE_DEDUP_MAX) {
|
|
30
|
+
for (const [k, ts] of _recentPushKeys) {
|
|
31
|
+
if (now - ts > STORE_DEDUP_TTL) _recentPushKeys.delete(k)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
interface MonitorState {
|
|
11
38
|
config: MonitorConfig
|
|
12
39
|
buffer: MonitorEvent[]
|
|
@@ -26,6 +53,10 @@ export const monitorStore = createStore<MonitorState>((set, get) => ({
|
|
|
26
53
|
_pausedUntil: 0,
|
|
27
54
|
|
|
28
55
|
push(event) {
|
|
56
|
+
// Global dedup: skip if an identical event was pushed recently
|
|
57
|
+
const dedupeKey = _pushDedupeKey(event)
|
|
58
|
+
if (_isRecentPush(dedupeKey)) return
|
|
59
|
+
|
|
29
60
|
const { config, buffer } = get()
|
|
30
61
|
const maxSize = config.maxBufferSize ?? 20
|
|
31
62
|
const sanitized: MonitorEvent = {
|