@djangocfg/devtools 2.1.548 → 2.1.549

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/devtools",
3
- "version": "2.1.548",
3
+ "version": "2.1.549",
4
4
  "description": "Frontend devtools for django-cfg apps: error/console capture with backend ingest, plus a floating debug panel. One package, one event stream.",
5
5
  "keywords": [
6
6
  "django",
@@ -49,16 +49,16 @@
49
49
  "lint": "eslint ."
50
50
  },
51
51
  "peerDependencies": {
52
- "@djangocfg/ui-core": "^2.1.548",
52
+ "@djangocfg/ui-core": "^2.1.549",
53
53
  "lucide-react": ">=0.400.0",
54
54
  "react": "^19.0.0",
55
55
  "react-dom": "^19.0.0",
56
56
  "zustand": "^5.0.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@djangocfg/eslint-config": "^2.1.548",
60
- "@djangocfg/typescript-config": "^2.1.548",
61
- "@djangocfg/ui-core": "^2.1.548",
59
+ "@djangocfg/eslint-config": "^2.1.549",
60
+ "@djangocfg/typescript-config": "^2.1.549",
61
+ "@djangocfg/ui-core": "^2.1.549",
62
62
  "@storybook/react-vite": "^10.5.0",
63
63
  "@types/node": "^25.9.5",
64
64
  "@types/react": "19.2.15",
package/src/capture.ts CHANGED
@@ -23,8 +23,17 @@ const HYDRATION_NOISE: RegExp[] = [
23
23
  /text content does not match server-rendered html/i,
24
24
  ]
25
25
 
26
+ // Third-party libraries that log a handled failure through console.error.
27
+ // The call already succeeded or fell back, so reporting it alerts on nothing.
28
+ const LIBRARY_NOISE: RegExp[] = [
29
+ // input-otp probes `:autofill`, which older engines reject, then installs
30
+ // the `-webkit-autofill` rule that actually applies. Nothing is broken.
31
+ /input-otp could not insert CSS rule/i,
32
+ ]
33
+
26
34
  function isNoise(msg: string, stack?: string): boolean {
27
35
  if (HYDRATION_NOISE.some((p) => p.test(msg))) return true
36
+ if (LIBRARY_NOISE.some((p) => p.test(msg))) return true
28
37
  // Our own transport failing must never loop back into capture.
29
38
  return INGEST_PATTERN.test(msg) || INGEST_PATTERN.test(stack ?? '')
30
39
  }
package/src/instance.ts CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  installJsErrorCapture,
14
14
  installValidationCapture,
15
15
  } from './capture'
16
- import { DEFAULT_FLUSH_INTERVAL, getSessionId, isDevelopment } from './internal'
16
+ import { DEFAULT_FLUSH_INTERVAL, getSessionId, isDevelopment, rawConsole } from './internal'
17
17
  import { devtoolsStore } from './store'
18
18
  import { EventLevel, EventType } from './types'
19
19
  import type { DevtoolsConfig, DevtoolsEvent, Logger } from './types'
@@ -145,9 +145,12 @@ export const devtools = {
145
145
  devtoolsStore.getState().addEntry({ level, source, message, data: cleanData, stack })
146
146
  }
147
147
  if (isDevelopment) {
148
+ // rawConsole, not console: `installConsoleCapture` wraps console.error
149
+ // and console.warn, so printing through them would feed this
150
+ // panel-only line back into capture and ingest it.
148
151
  const method = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log'
149
- if (cleanData) console[method](`[${source}]`, message, cleanData)
150
- else console[method](`[${source}]`, message)
152
+ if (cleanData) rawConsole[method](`[${source}]`, message, cleanData)
153
+ else rawConsole[method](`[${source}]`, message)
151
154
  }
152
155
  }
153
156
  return {
package/src/internal.ts CHANGED
@@ -83,3 +83,16 @@ export function makeDeduper(max = 200): (key: string, ttl: number) => boolean {
83
83
  export function truncate(s: string, max: number): string {
84
84
  return s.length > max ? s.slice(0, max - 1) + '…' : s
85
85
  }
86
+
87
+ /**
88
+ * The console as it was before `installConsoleCapture` wrapped it.
89
+ *
90
+ * Bound at module load, which runs before any install, so callers that must
91
+ * NOT feed capture — the local logger — can still print. Going through the
92
+ * wrapped console instead turns a panel-only log line into an ingested event.
93
+ */
94
+ export const rawConsole = {
95
+ log: console.log.bind(console),
96
+ warn: console.warn.bind(console),
97
+ error: console.error.bind(console),
98
+ }
package/src/server.ts CHANGED
@@ -27,10 +27,13 @@ let _config: ServerDevtoolsConfig = {}
27
27
 
28
28
  async function send(events: DevtoolsEvent[]): Promise<void> {
29
29
  try {
30
+ // `??`, not a spread of the config underneath: an event carrying an
31
+ // explicit `project_name: undefined` would otherwise blank the configured
32
+ // one. Same precedence as the browser store.
30
33
  await sendBatch(_config.baseUrl ?? '', events.map((e) => ({
31
- project_name: _config.project,
32
- environment: _config.environment,
33
34
  ...e,
35
+ project_name: e.project_name ?? _config.project,
36
+ environment: e.environment ?? _config.environment,
34
37
  })))
35
38
  } catch {
36
39
  // Never throw from error reporting
package/src/store.ts CHANGED
@@ -24,6 +24,9 @@ import type { DevtoolsConfig, DevtoolsEvent, LogEntry, LogLevel } from './types'
24
24
 
25
25
  const MAX_ENTRIES = 1000
26
26
  const MAX_BATCH = 25
27
+ // Ceiling for events held while ingest is failing. The oldest are dropped
28
+ // first: during an outage the newest events describe the current state.
29
+ const MAX_OUTBOX = 200
27
30
 
28
31
  // Circuit breaker: pause ingest after N consecutive transport failures
29
32
  const BREAKER_THRESHOLD = 3
@@ -152,7 +155,14 @@ export const devtoolsStore = createStore<DevtoolsState>((set, get) => ({
152
155
  () => set({ _failures: 0, _pausedUntil: 0 }),
153
156
  () => {
154
157
  const failures = get()._failures + 1
158
+ // Put the batch back, ahead of whatever arrived meanwhile: a failed
159
+ // send must not consume the events. Dropping them here would lose
160
+ // exactly the reports of an incident the backend was too busy to take.
161
+ // A beacon flush is the exception — the page is going away and there
162
+ // is no later attempt to hand them to.
163
+ const requeued = useBeacon ? get().outbox : [...batch, ...get().outbox]
155
164
  set({
165
+ outbox: requeued.length > MAX_OUTBOX ? requeued.slice(-MAX_OUTBOX) : requeued,
156
166
  _failures: failures,
157
167
  _pausedUntil:
158
168
  failures >= BREAKER_THRESHOLD ? Date.now() + BREAKER_COOLDOWN_MS : get()._pausedUntil,