@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/monitor",
3
- "version": "2.1.236",
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.236",
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 { config } = monitorStore.getState()
33
- monitorStore.getState().push({
34
- event_type: EventType.NETWORK_ERROR,
35
- level: EventLevel.ERROR,
36
- message: err instanceof Error ? err.message : `Network error — ${method} ${url}`,
37
- url: typeof window !== 'undefined' ? window.location.href : '',
38
- http_method: method,
39
- http_url: url,
40
- session_id: getSessionId(),
41
- user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
42
- project_name: config.project,
43
- environment: config.environment,
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 = {