@djangocfg/monitor 2.1.237 → 2.1.239

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/index.d.cts CHANGED
@@ -85,6 +85,10 @@ interface MonitorConfig {
85
85
  captureJsErrors?: boolean;
86
86
  /** Log debug info to console. Default: false */
87
87
  debug?: boolean;
88
+ /** Deduplication TTL in ms for per-capture-source filtering. Default: 5000 */
89
+ dedupeTtl?: number;
90
+ /** Deduplication TTL in ms for the global store-level filter (cross-source). Default: 30000 */
91
+ dedupeStoreTtl?: number;
88
92
  }
89
93
  interface ServerMonitorConfig {
90
94
  /** Base URL for the django-cfg backend (absolute URL required on server) */
package/dist/index.d.ts CHANGED
@@ -85,6 +85,10 @@ interface MonitorConfig {
85
85
  captureJsErrors?: boolean;
86
86
  /** Log debug info to console. Default: false */
87
87
  debug?: boolean;
88
+ /** Deduplication TTL in ms for per-capture-source filtering. Default: 5000 */
89
+ dedupeTtl?: number;
90
+ /** Deduplication TTL in ms for the global store-level filter (cross-source). Default: 30000 */
91
+ dedupeStoreTtl?: number;
88
92
  }
89
93
  interface ServerMonitorConfig {
90
94
  /** Base URL for the django-cfg backend (absolute URL required on server) */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/monitor",
3
- "version": "2.1.237",
3
+ "version": "2.1.239",
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.237",
86
+ "@djangocfg/typescript-config": "^2.1.239",
87
87
  "p-retry": "^6.2.0",
88
88
  "@types/node": "^24.7.2",
89
89
  "@types/react": "^19.1.0",
@@ -1,7 +1,7 @@
1
1
  import { computeFingerprint } from './fingerprint'
2
2
  import { getSessionId } from './session'
3
3
  import { monitorStore } from '../store'
4
- import { MONITOR_INGEST_PATTERN } from '../constants'
4
+ import { MONITOR_INGEST_PATTERN, DEFAULT_DEDUPE_TTL } from '../constants'
5
5
  import { EventType, EventLevel } from '../../_api'
6
6
  import type { FrontendEventIngestRequestEventType, FrontendEventIngestRequestLevel } from '../../_api/generated/cfg_monitor/enums'
7
7
 
@@ -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 (read from config)
22
+ const recentConsoleFingerprints = new Map<string, number>()
23
+
24
+ function isRecentConsole(fingerprint: string): boolean {
25
+ const ttl = monitorStore.getState().config.dedupeTtl ?? DEFAULT_DEDUPE_TTL
26
+ const now = Date.now()
27
+ const last = recentConsoleFingerprints.get(fingerprint)
28
+ if (last !== undefined && now - last < 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],
@@ -1,7 +1,7 @@
1
1
  import { computeFingerprint } from './fingerprint'
2
2
  import { getSessionId } from './session'
3
3
  import { monitorStore } from '../store'
4
- import { MONITOR_INGEST_PATTERN } from '../constants'
4
+ import { MONITOR_INGEST_PATTERN, DEFAULT_DEDUPE_TTL } from '../constants'
5
5
  import { EventType, EventLevel } from '../../_api'
6
6
 
7
7
  const MSG_MAX = 2000
@@ -29,17 +29,16 @@ function isHydrationNoise(msg: string): boolean {
29
29
  return HYDRATION_NOISE.some((p) => p.test(msg))
30
30
  }
31
31
 
32
- // Client-side dedup: skip re-sending the same fingerprint within CLIENT_DEDUP_TTL ms.
32
+ // Client-side dedup: skip re-sending the same fingerprint within dedupeTtl.
33
33
  // Protects against crash-loop pages flooding the ingest endpoint.
34
- const CLIENT_DEDUP_TTL = 5 * 60 * 1000 // 5 minutes
35
34
  const recentFingerprints = new Map<string, number>()
36
35
 
37
36
  function isRecentlySent(fingerprint: string): boolean {
37
+ const ttl = monitorStore.getState().config.dedupeTtl ?? DEFAULT_DEDUPE_TTL
38
38
  const now = Date.now()
39
39
  const last = recentFingerprints.get(fingerprint)
40
- if (last !== undefined && now - last < CLIENT_DEDUP_TTL) return true
40
+ if (last !== undefined && now - last < ttl) return true
41
41
  recentFingerprints.set(fingerprint, now)
42
- // Cap map size to avoid memory growth — evict oldest entry when over limit
43
42
  if (recentFingerprints.size > 100) {
44
43
  const oldest = [...recentFingerprints.entries()].sort((a, b) => a[1] - b[1])[0]
45
44
  recentFingerprints.delete(oldest[0])
@@ -1,7 +1,25 @@
1
1
  import { getSessionId } from './session'
2
2
  import { monitorStore } from '../store'
3
+ import { DEFAULT_DEDUPE_TTL } from '../constants'
3
4
  import { EventType, EventLevel } from '../../_api'
4
5
 
6
+ // Dedup: skip re-sending the same network error within TTL (read from config)
7
+ const recentNetworkErrors = new Map<string, number>()
8
+
9
+ function isRecentNetwork(key: string): boolean {
10
+ const ttl = monitorStore.getState().config.dedupeTtl ?? DEFAULT_DEDUPE_TTL
11
+ const now = Date.now()
12
+ const last = recentNetworkErrors.get(key)
13
+ if (last !== undefined && now - last < ttl) return true
14
+ recentNetworkErrors.set(key, now)
15
+ if (recentNetworkErrors.size > 100) {
16
+ for (const [k, ts] of recentNetworkErrors) {
17
+ if (now - ts > ttl) recentNetworkErrors.delete(k)
18
+ }
19
+ }
20
+ return false
21
+ }
22
+
5
23
  export async function monitoredFetch(
6
24
  input: RequestInfo | URL,
7
25
  init?: RequestInit,
@@ -12,6 +30,8 @@ export async function monitoredFetch(
12
30
  try {
13
31
  const response = await fetch(input, init)
14
32
  if (!response.ok) {
33
+ const netKey = `${response.status}:${method}:${url}`
34
+ if (isRecentNetwork(netKey)) return response
15
35
  const { config } = monitorStore.getState()
16
36
  monitorStore.getState().push({
17
37
  event_type: EventType.NETWORK_ERROR,
@@ -29,19 +49,23 @@ export async function monitoredFetch(
29
49
  }
30
50
  return response
31
51
  } 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
- })
52
+ const errMsg = err instanceof Error ? err.message : 'network-error'
53
+ const netKey = `err:${errMsg.slice(0, 50)}:${method}:${url}`
54
+ if (!isRecentNetwork(netKey)) {
55
+ const { config } = monitorStore.getState()
56
+ monitorStore.getState().push({
57
+ event_type: EventType.NETWORK_ERROR,
58
+ level: EventLevel.ERROR,
59
+ message: err instanceof Error ? err.message : `Network error — ${method} ${url}`,
60
+ url: typeof window !== 'undefined' ? window.location.href : '',
61
+ http_method: method,
62
+ http_url: url,
63
+ session_id: getSessionId(),
64
+ user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
65
+ project_name: config.project,
66
+ environment: config.environment,
67
+ })
68
+ }
45
69
  throw err
46
70
  }
47
71
  }
@@ -1,7 +1,25 @@
1
1
  import { getSessionId } from './session'
2
2
  import { monitorStore } from '../store'
3
+ import { DEFAULT_DEDUPE_TTL } from '../constants'
3
4
  import { EventType, EventLevel } from '../../_api'
4
5
 
6
+ // Dedup: skip re-sending the same validation error within TTL (read from config)
7
+ const recentValidations = new Map<string, number>()
8
+
9
+ function isRecentValidation(key: string): boolean {
10
+ const ttl = monitorStore.getState().config.dedupeTtl ?? DEFAULT_DEDUPE_TTL
11
+ const now = Date.now()
12
+ const last = recentValidations.get(key)
13
+ if (last !== undefined && now - last < ttl) return true
14
+ recentValidations.set(key, now)
15
+ if (recentValidations.size > 50) {
16
+ for (const [k, ts] of recentValidations) {
17
+ if (now - ts > ttl) recentValidations.delete(k)
18
+ }
19
+ }
20
+ return false
21
+ }
22
+
5
23
  interface ValidationErrorDetail {
6
24
  operation: string
7
25
  path: string
@@ -16,6 +34,8 @@ export function installValidationCapture(): () => void {
16
34
  if (!(event instanceof CustomEvent)) return
17
35
  try {
18
36
  const detail = event.detail as ValidationErrorDetail
37
+ const dedupeKey = `${detail.operation}:${detail.path}:${detail.method}`
38
+ if (isRecentValidation(dedupeKey)) return
19
39
  const { config } = monitorStore.getState()
20
40
  const rawMsg = `Zod validation error in ${detail.operation}: ${detail.error?.message ?? 'unknown'}`
21
41
  monitorStore.getState().push({
@@ -5,3 +5,9 @@
5
5
 
6
6
  /** Matches any request to the monitor ingest endpoint */
7
7
  export const MONITOR_INGEST_PATTERN = /cfg\/monitor\/ingest/
8
+
9
+ /** Default deduplication TTL for per-capture-source filtering (ms) */
10
+ export const DEFAULT_DEDUPE_TTL = 5_000
11
+
12
+ /** Default deduplication TTL for global store-level filtering (ms) */
13
+ export const DEFAULT_DEDUPE_STORE_TTL = 30_000
@@ -2,11 +2,36 @@ import { createStore } from 'zustand/vanilla'
2
2
  import type { MonitorEvent, MonitorConfig } from '../../types'
3
3
  import { sendBatch } from '../transport/ingest'
4
4
  import { MONITOR_VERSION } from '../utils/env'
5
+ import { DEFAULT_DEDUPE_STORE_TTL } from '../constants'
5
6
 
6
7
  // Circuit breaker: pause flushing after N consecutive transport failures
7
8
  const CIRCUIT_BREAKER_THRESHOLD = 3
8
9
  const CIRCUIT_BREAKER_COOLDOWN_MS = 60_000 // 1 minute
9
10
 
11
+ // Global deduplication: skip pushing identical events within this window.
12
+ // Protects against multiple capture sources (js-errors, console, ErrorTrackingProvider)
13
+ // all firing for the same underlying error.
14
+ const STORE_DEDUP_MAX = 200
15
+ const _recentPushKeys = new Map<string, number>()
16
+
17
+ function _pushDedupeKey(event: MonitorEvent): string {
18
+ const msg = (event.message ?? '').slice(0, 100)
19
+ return `${event.event_type}:${event.level}:${msg}:${event.http_url ?? event.url ?? ''}`
20
+ }
21
+
22
+ function _isRecentPush(key: string, ttl: number): boolean {
23
+ const now = Date.now()
24
+ const last = _recentPushKeys.get(key)
25
+ if (last !== undefined && now - last < ttl) return true
26
+ _recentPushKeys.set(key, now)
27
+ if (_recentPushKeys.size > STORE_DEDUP_MAX) {
28
+ for (const [k, ts] of _recentPushKeys) {
29
+ if (now - ts > ttl) _recentPushKeys.delete(k)
30
+ }
31
+ }
32
+ return false
33
+ }
34
+
10
35
  interface MonitorState {
11
36
  config: MonitorConfig
12
37
  buffer: MonitorEvent[]
@@ -26,7 +51,11 @@ export const monitorStore = createStore<MonitorState>((set, get) => ({
26
51
  _pausedUntil: 0,
27
52
 
28
53
  push(event) {
54
+ // Global dedup: skip if an identical event was pushed recently
29
55
  const { config, buffer } = get()
56
+ const storeTtl = config.dedupeStoreTtl ?? DEFAULT_DEDUPE_STORE_TTL
57
+ const dedupeKey = _pushDedupeKey(event)
58
+ if (_isRecentPush(dedupeKey, storeTtl)) return
30
59
  const maxSize = config.maxBufferSize ?? 20
31
60
  const sanitized: MonitorEvent = {
32
61
  build_id: event.build_id ?? config.buildId ?? `sdk:${MONITOR_VERSION}`,
@@ -21,6 +21,10 @@ export interface MonitorConfig {
21
21
  captureJsErrors?: boolean
22
22
  /** Log debug info to console. Default: false */
23
23
  debug?: boolean
24
+ /** Deduplication TTL in ms for per-capture-source filtering. Default: 5000 */
25
+ dedupeTtl?: number
26
+ /** Deduplication TTL in ms for the global store-level filter (cross-source). Default: 30000 */
27
+ dedupeStoreTtl?: number
24
28
  }
25
29
 
26
30
  export interface ServerMonitorConfig {