@djangocfg/devtools 2.1.469 → 2.1.471

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.
Files changed (2) hide show
  1. package/package.json +4 -4
  2. package/src/ingest.ts +32 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/devtools",
3
- "version": "2.1.469",
3
+ "version": "2.1.471",
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",
@@ -48,15 +48,15 @@
48
48
  "lint": "eslint ."
49
49
  },
50
50
  "peerDependencies": {
51
- "@djangocfg/ui-core": "^2.1.469",
51
+ "@djangocfg/ui-core": "^2.1.471",
52
52
  "lucide-react": ">=0.400.0",
53
53
  "react": "^19.2.4",
54
54
  "react-dom": "^19.2.4",
55
55
  "zustand": "^5.0.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@djangocfg/typescript-config": "^2.1.469",
59
- "@djangocfg/ui-core": "^2.1.469",
58
+ "@djangocfg/typescript-config": "^2.1.471",
59
+ "@djangocfg/ui-core": "^2.1.471",
60
60
  "@types/node": "^25.9.5",
61
61
  "@types/react": "19.2.15",
62
62
  "lucide-react": "^0.545.0",
package/src/ingest.ts CHANGED
@@ -2,8 +2,34 @@
2
2
  * Ingest transport — one plain fetch to `POST /cfg/monitor/ingest/`.
3
3
  *
4
4
  * The endpoint is AllowAny + rate-limited on the backend and always answers
5
- * 202; nothing here needs auth headers or a generated client. `keepalive` is
6
- * used on page-unload flushes so the request survives the document.
5
+ * 202; nothing here needs auth headers or a generated client.
6
+ *
7
+ * ─────────────────────────────────────────────────────────────────────────────
8
+ * WHY THIS DOES NOT USE @djangocfg/analytics' BeaconTransport
9
+ * ─────────────────────────────────────────────────────────────────────────────
10
+ * It would be the obvious dedup, and it is wrong: BeaconTransport is
11
+ * fire-and-forget, while `sendBatch` MUST reject on failure — `store.ts` drives
12
+ * a circuit breaker off that rejection (BREAKER_THRESHOLD / BREAKER_COOLDOWN_MS),
13
+ * and `server.ts` awaits it outside the browser entirely. Swapping in a
14
+ * fire-and-forget transport would silently disable the breaker, so a backend
15
+ * outage would turn into an unbounded retry loop from every open tab.
16
+ *
17
+ * What IS shared is the rule below, and it is the part that was actually broken.
18
+ *
19
+ * ─────────────────────────────────────────────────────────────────────────────
20
+ * THE UNLOAD PATH MUST NOT SEND application/json
21
+ * ─────────────────────────────────────────────────────────────────────────────
22
+ * `application/json` is NOT CORS-safelisted, so a cross-origin request carrying
23
+ * it leaves no-cors mode and triggers an OPTIONS preflight. During page unload
24
+ * that preflight usually cannot complete, so the POST is never sent — and the
25
+ * events lost are the ones that mattered most: whatever crashed the page and
26
+ * caused the navigation away.
27
+ *
28
+ * `text/plain` IS CORS-safelisted, so the same JSON body is delivered with no
29
+ * preflight. The body is unchanged; only the label differs. The server reads it
30
+ * with an additive parser scoped to the ingest view.
31
+ *
32
+ * Same trap, same fix, as @djangocfg/analytics. Verified in Chrome 149.
7
33
  */
8
34
 
9
35
  import { INGEST_PATH } from './internal'
@@ -18,7 +44,10 @@ export async function sendBatch(
18
44
 
19
45
  const res = await fetch(`${baseUrl}${INGEST_PATH}`, {
20
46
  method: 'POST',
21
- headers: { 'Content-Type': 'application/json' },
47
+ headers: {
48
+ // Unload flushes must stay CORS-simple or they never leave the browser.
49
+ 'Content-Type': useBeacon ? 'text/plain' : 'application/json',
50
+ },
22
51
  body: JSON.stringify({ events }),
23
52
  credentials: 'include',
24
53
  ...(useBeacon ? { keepalive: true } : {}),