@djangocfg/monitor 2.1.216

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 (62) hide show
  1. package/README.md +341 -0
  2. package/dist/client.cjs +1273 -0
  3. package/dist/client.cjs.map +1 -0
  4. package/dist/client.d.cts +123 -0
  5. package/dist/client.d.ts +123 -0
  6. package/dist/client.mjs +1243 -0
  7. package/dist/client.mjs.map +1 -0
  8. package/dist/index.cjs +18 -0
  9. package/dist/index.cjs.map +1 -0
  10. package/dist/index.d.cts +101 -0
  11. package/dist/index.d.ts +101 -0
  12. package/dist/index.mjs +1 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/server.cjs +947 -0
  15. package/dist/server.cjs.map +1 -0
  16. package/dist/server.d.cts +117 -0
  17. package/dist/server.d.ts +117 -0
  18. package/dist/server.mjs +917 -0
  19. package/dist/server.mjs.map +1 -0
  20. package/package.json +82 -0
  21. package/src/.claude/.sidecar/activity.jsonl +1 -0
  22. package/src/.claude/.sidecar/map_cache.json +38 -0
  23. package/src/.claude/.sidecar/usage.json +5 -0
  24. package/src/.claude/project-map.md +29 -0
  25. package/src/_api/BaseClient.ts +18 -0
  26. package/src/_api/generated/cfg_monitor/CLAUDE.md +60 -0
  27. package/src/_api/generated/cfg_monitor/_utils/fetchers/index.ts +30 -0
  28. package/src/_api/generated/cfg_monitor/_utils/fetchers/monitor.ts +51 -0
  29. package/src/_api/generated/cfg_monitor/_utils/hooks/index.ts +30 -0
  30. package/src/_api/generated/cfg_monitor/_utils/hooks/monitor.ts +43 -0
  31. package/src/_api/generated/cfg_monitor/_utils/schemas/FrontendEventIngestRequest.schema.ts +34 -0
  32. package/src/_api/generated/cfg_monitor/_utils/schemas/IngestBatchRequest.schema.ts +20 -0
  33. package/src/_api/generated/cfg_monitor/_utils/schemas/index.ts +22 -0
  34. package/src/_api/generated/cfg_monitor/api-instance.ts +181 -0
  35. package/src/_api/generated/cfg_monitor/client.ts +322 -0
  36. package/src/_api/generated/cfg_monitor/enums.ts +36 -0
  37. package/src/_api/generated/cfg_monitor/errors.ts +118 -0
  38. package/src/_api/generated/cfg_monitor/http.ts +137 -0
  39. package/src/_api/generated/cfg_monitor/index.ts +317 -0
  40. package/src/_api/generated/cfg_monitor/logger.ts +261 -0
  41. package/src/_api/generated/cfg_monitor/monitor/client.ts +25 -0
  42. package/src/_api/generated/cfg_monitor/monitor/index.ts +4 -0
  43. package/src/_api/generated/cfg_monitor/monitor/models.ts +48 -0
  44. package/src/_api/generated/cfg_monitor/retry.ts +177 -0
  45. package/src/_api/generated/cfg_monitor/schema.json +184 -0
  46. package/src/_api/generated/cfg_monitor/storage.ts +163 -0
  47. package/src/_api/generated/cfg_monitor/validation-events.ts +135 -0
  48. package/src/_api/index.ts +6 -0
  49. package/src/client/capture/console.ts +72 -0
  50. package/src/client/capture/fingerprint.ts +27 -0
  51. package/src/client/capture/js-errors.ts +70 -0
  52. package/src/client/capture/network.ts +47 -0
  53. package/src/client/capture/session.ts +33 -0
  54. package/src/client/capture/validation.ts +38 -0
  55. package/src/client/index.ts +72 -0
  56. package/src/client/store/index.ts +41 -0
  57. package/src/client/transport/ingest.ts +31 -0
  58. package/src/index.ts +12 -0
  59. package/src/server/index.ts +85 -0
  60. package/src/types/config.ts +33 -0
  61. package/src/types/events.ts +5 -0
  62. package/src/types/index.ts +6 -0
@@ -0,0 +1,135 @@
1
+ // @ts-nocheck
2
+ // Auto-generated by DjangoCFG - see CLAUDE.md
3
+ /**
4
+ * Zod Validation Events - Browser CustomEvent integration
5
+ *
6
+ * Dispatches browser CustomEvents when Zod validation fails, allowing
7
+ * React/frontend apps to listen and handle validation errors globally.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * // In your React app
12
+ * window.addEventListener('zod-validation-error', (event) => {
13
+ * const { operation, path, method, error, response } = event.detail;
14
+ * console.error(`Validation failed for ${method} ${path}`, error);
15
+ * // Show toast notification, log to Sentry, etc.
16
+ * });
17
+ * ```
18
+ */
19
+
20
+ import type { ZodError } from 'zod'
21
+
22
+ /**
23
+ * Validation error event detail
24
+ */
25
+ export interface ValidationErrorDetail {
26
+ /** Operation/function name that failed validation */
27
+ operation: string
28
+ /** API endpoint path */
29
+ path: string
30
+ /** HTTP method */
31
+ method: string
32
+ /** Zod validation error */
33
+ error: ZodError
34
+ /** Raw response data that failed validation */
35
+ response: any
36
+ /** Timestamp of the error */
37
+ timestamp: Date
38
+ }
39
+
40
+ /**
41
+ * Custom event type for Zod validation errors
42
+ */
43
+ export type ValidationErrorEvent = CustomEvent<ValidationErrorDetail>
44
+
45
+ /**
46
+ * Dispatch a Zod validation error event.
47
+ *
48
+ * Only dispatches in browser environment (when window is defined).
49
+ * Safe to call in Node.js/SSR - will be a no-op.
50
+ *
51
+ * @param detail - Validation error details
52
+ */
53
+ export function dispatchValidationError(detail: ValidationErrorDetail): void {
54
+ // Check if running in browser
55
+ if (typeof window === 'undefined') {
56
+ return
57
+ }
58
+
59
+ try {
60
+ const event = new CustomEvent<ValidationErrorDetail>('zod-validation-error', {
61
+ detail,
62
+ bubbles: true,
63
+ cancelable: false,
64
+ })
65
+
66
+ window.dispatchEvent(event)
67
+ } catch (error) {
68
+ // Silently fail - validation event dispatch should never crash the app
69
+ console.warn('Failed to dispatch validation error event:', error)
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Add a global listener for Zod validation errors.
75
+ *
76
+ * @param callback - Function to call when validation error occurs
77
+ * @returns Cleanup function to remove the listener
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * const cleanup = onValidationError(({ operation, error }) => {
82
+ * toast.error(`Validation failed in ${operation}`);
83
+ * logToSentry(error);
84
+ * });
85
+ *
86
+ * // Later, remove listener
87
+ * cleanup();
88
+ * ```
89
+ */
90
+ export function onValidationError(
91
+ callback: (detail: ValidationErrorDetail) => void
92
+ ): () => void {
93
+ if (typeof window === 'undefined') {
94
+ // Return no-op cleanup function for SSR
95
+ return () => {}
96
+ }
97
+
98
+ const handler = (event: Event) => {
99
+ if (event instanceof CustomEvent) {
100
+ callback(event.detail)
101
+ }
102
+ }
103
+
104
+ window.addEventListener('zod-validation-error', handler)
105
+
106
+ // Return cleanup function
107
+ return () => {
108
+ window.removeEventListener('zod-validation-error', handler)
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Format Zod error for logging/display.
114
+ *
115
+ * @param error - Zod validation error
116
+ * @returns Formatted error message
117
+ */
118
+ export function formatZodError(error: ZodError): string {
119
+ const issues = error.issues.map((issue, index) => {
120
+ const path = issue.path.join('.') || 'root'
121
+ const parts = [`${index + 1}. ${path}: ${issue.message}`]
122
+
123
+ if ('expected' in issue && issue.expected) {
124
+ parts.push(` Expected: ${issue.expected}`)
125
+ }
126
+
127
+ if ('received' in issue && issue.received) {
128
+ parts.push(` Received: ${issue.received}`)
129
+ }
130
+
131
+ return parts.join('\n')
132
+ })
133
+
134
+ return issues.join('\n')
135
+ }
@@ -0,0 +1,6 @@
1
+ export { monitorApi, configureMonitorApi, BaseClient } from './BaseClient'
2
+ export type { FrontendEventIngestRequest, IngestBatchRequest } from './generated/cfg_monitor/monitor/models'
3
+ export { FrontendEventIngestRequestEventType as EventType, FrontendEventIngestRequestLevel as EventLevel } from './generated/cfg_monitor/enums'
4
+
5
+ /** Ingest path — matches the generated client. Single source of truth for sendBeacon fallback. */
6
+ export const INGEST_PATH = '/cfg/monitor/ingest/'
@@ -0,0 +1,72 @@
1
+ import { computeFingerprint } from './fingerprint'
2
+ import { getSessionId } from './session'
3
+ import { monitorStore } from '../store'
4
+ import { EventType, EventLevel } from '../../_api'
5
+ import type { FrontendEventIngestRequestEventType, FrontendEventIngestRequestLevel } from '../../_api/generated/cfg_monitor/enums'
6
+
7
+ type ConsoleLevel = 'warn' | 'error'
8
+
9
+ const levelMap: Record<ConsoleLevel, FrontendEventIngestRequestLevel> = {
10
+ warn: EventLevel.WARN,
11
+ error: EventLevel.ERROR,
12
+ }
13
+ const typeMap: Record<ConsoleLevel, FrontendEventIngestRequestEventType> = {
14
+ warn: EventType.WARNING,
15
+ error: EventType.ERROR,
16
+ }
17
+
18
+ function stringify(args: unknown[]): string {
19
+ return args.map((a) => {
20
+ if (typeof a === 'string') return a
21
+ if (a instanceof Error) return a.message
22
+ try { return JSON.stringify(a) } catch { return String(a) }
23
+ }).join(' ')
24
+ }
25
+
26
+ async function captureConsoleEvent(level: ConsoleLevel, args: unknown[]) {
27
+ try {
28
+ const message = stringify(args)
29
+ const url = typeof window !== 'undefined' ? window.location.href : ''
30
+ const fingerprint = await computeFingerprint(message, '', url)
31
+ const { config } = monitorStore.getState()
32
+ monitorStore.getState().push({
33
+ event_type: typeMap[level],
34
+ level: levelMap[level],
35
+ message,
36
+ url,
37
+ fingerprint,
38
+ session_id: getSessionId(),
39
+ user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
40
+ project_name: config.project,
41
+ environment: config.environment,
42
+ })
43
+ } catch { /* never crash */ }
44
+ }
45
+
46
+ export function installConsoleCapture(): () => void {
47
+ if (typeof window === 'undefined') return () => {}
48
+
49
+ try {
50
+ const g = globalThis as Record<string, unknown>
51
+ if (g.consola && typeof (g.consola as { addReporter?: unknown }).addReporter === 'function') {
52
+ const consolaInst = g.consola as {
53
+ addReporter: (r: unknown) => void
54
+ removeReporter: (r: unknown) => void
55
+ }
56
+ const reporter = {
57
+ log(logObj: { level: number; args: unknown[] }) {
58
+ if (logObj.level === 1) captureConsoleEvent('error', logObj.args)
59
+ else if (logObj.level === 2) captureConsoleEvent('warn', logObj.args)
60
+ },
61
+ }
62
+ consolaInst.addReporter(reporter)
63
+ return () => consolaInst.removeReporter(reporter)
64
+ }
65
+ } catch { /* consola not available */ }
66
+
67
+ const origWarn = console.warn.bind(console)
68
+ const origError = console.error.bind(console)
69
+ console.warn = (...args: unknown[]) => { origWarn(...args); captureConsoleEvent('warn', args) }
70
+ console.error = (...args: unknown[]) => { origError(...args); captureConsoleEvent('error', args) }
71
+ return () => { console.warn = origWarn; console.error = origError }
72
+ }
@@ -0,0 +1,27 @@
1
+ function simpleHash(str: string): string {
2
+ let hash = 0
3
+ for (let i = 0; i < str.length; i++) {
4
+ hash = (hash << 5) - hash + str.charCodeAt(i)
5
+ hash = hash & hash
6
+ }
7
+ return Math.abs(hash).toString(16).padStart(8, '0')
8
+ }
9
+
10
+ export async function computeFingerprint(
11
+ message: string,
12
+ stack: string,
13
+ url: string,
14
+ ): Promise<string> {
15
+ const raw = `${message}|${stack}|${url}`
16
+ if (typeof crypto !== 'undefined' && crypto.subtle) {
17
+ try {
18
+ const data = new TextEncoder().encode(raw)
19
+ const buf = await crypto.subtle.digest('SHA-256', data)
20
+ return Array.from(new Uint8Array(buf))
21
+ .map((b) => b.toString(16).padStart(2, '0'))
22
+ .join('')
23
+ .slice(0, 64)
24
+ } catch { /* fall through */ }
25
+ }
26
+ return simpleHash(raw)
27
+ }
@@ -0,0 +1,70 @@
1
+ import { computeFingerprint } from './fingerprint'
2
+ import { getSessionId } from './session'
3
+ import { monitorStore } from '../store'
4
+ import { EventType, EventLevel } from '../../_api'
5
+
6
+ export function installJsErrorCapture(): () => void {
7
+ if (typeof window === 'undefined') return () => {}
8
+
9
+ const onError = async (
10
+ message: string | Event,
11
+ source?: string,
12
+ lineno?: number,
13
+ colno?: number,
14
+ error?: Error,
15
+ ) => {
16
+ try {
17
+ const msg = typeof message === 'string' ? message : String(message)
18
+ const stack = error?.stack ?? `at ${source}:${lineno}:${colno}`
19
+ const url = window.location.href
20
+ const fingerprint = await computeFingerprint(msg, stack, url)
21
+ const { config } = monitorStore.getState()
22
+ monitorStore.getState().push({
23
+ event_type: EventType.JS_ERROR,
24
+ level: EventLevel.ERROR,
25
+ message: msg,
26
+ stack_trace: stack,
27
+ url,
28
+ fingerprint,
29
+ session_id: getSessionId(),
30
+ user_agent: navigator.userAgent,
31
+ project_name: config.project,
32
+ environment: config.environment,
33
+ })
34
+ } catch { /* never crash */ }
35
+ }
36
+
37
+ const onUnhandledRejection = async (e: PromiseRejectionEvent) => {
38
+ try {
39
+ const reason = e.reason
40
+ const msg = reason instanceof Error ? reason.message
41
+ : typeof reason === 'string' ? reason
42
+ : 'Unhandled promise rejection'
43
+ const stack = reason instanceof Error ? (reason.stack ?? '') : ''
44
+ const url = window.location.href
45
+ const fingerprint = await computeFingerprint(msg, stack, url)
46
+ const { config } = monitorStore.getState()
47
+ monitorStore.getState().push({
48
+ event_type: EventType.JS_ERROR,
49
+ level: EventLevel.ERROR,
50
+ message: msg,
51
+ stack_trace: stack,
52
+ url,
53
+ fingerprint,
54
+ session_id: getSessionId(),
55
+ user_agent: navigator.userAgent,
56
+ project_name: config.project,
57
+ environment: config.environment,
58
+ })
59
+ } catch { /* never crash */ }
60
+ }
61
+
62
+ const errHandler = (e: ErrorEvent) => onError(e.message, e.filename, e.lineno, e.colno, e.error)
63
+ window.addEventListener('error', errHandler)
64
+ window.addEventListener('unhandledrejection', onUnhandledRejection)
65
+
66
+ return () => {
67
+ window.removeEventListener('error', errHandler)
68
+ window.removeEventListener('unhandledrejection', onUnhandledRejection)
69
+ }
70
+ }
@@ -0,0 +1,47 @@
1
+ import { getSessionId } from './session'
2
+ import { monitorStore } from '../store'
3
+ import { EventType, EventLevel } from '../../_api'
4
+
5
+ export async function monitoredFetch(
6
+ input: RequestInfo | URL,
7
+ init?: RequestInit,
8
+ ): Promise<Response> {
9
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
10
+ const method = (init?.method ?? 'GET').toUpperCase()
11
+
12
+ try {
13
+ const response = await fetch(input, init)
14
+ if (!response.ok) {
15
+ const { config } = monitorStore.getState()
16
+ monitorStore.getState().push({
17
+ event_type: EventType.NETWORK_ERROR,
18
+ level: response.status >= 500 ? EventLevel.ERROR : EventLevel.WARN,
19
+ message: `HTTP ${response.status} ${response.statusText} — ${method} ${url}`,
20
+ url: typeof window !== 'undefined' ? window.location.href : '',
21
+ http_status: response.status,
22
+ http_method: method,
23
+ http_url: url,
24
+ session_id: getSessionId(),
25
+ user_agent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
26
+ project_name: config.project,
27
+ environment: config.environment,
28
+ })
29
+ }
30
+ return response
31
+ } 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
+ })
45
+ throw err
46
+ }
47
+ }
@@ -0,0 +1,33 @@
1
+ const SESSION_KEY = 'fm_session_id'
2
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 365
3
+
4
+ function generateUUID(): string {
5
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
6
+ return crypto.randomUUID()
7
+ }
8
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
9
+ const r = (Math.random() * 16) | 0
10
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
11
+ })
12
+ }
13
+
14
+ function setCookie(name: string, value: string): void {
15
+ if (typeof document === 'undefined') return
16
+ document.cookie = `${name}=${value}; path=/; SameSite=Lax; max-age=${COOKIE_MAX_AGE}`
17
+ }
18
+
19
+ export function getSessionId(): string {
20
+ if (typeof localStorage === 'undefined') return ''
21
+ let id = localStorage.getItem(SESSION_KEY)
22
+ if (!id) {
23
+ id = generateUUID()
24
+ localStorage.setItem(SESSION_KEY, id)
25
+ setCookie(SESSION_KEY, id)
26
+ }
27
+ return id
28
+ }
29
+
30
+ export function ensureSessionCookie(): void {
31
+ const id = getSessionId()
32
+ if (id) setCookie(SESSION_KEY, id)
33
+ }
@@ -0,0 +1,38 @@
1
+ import { getSessionId } from './session'
2
+ import { monitorStore } from '../store'
3
+ import { EventType, EventLevel } from '../../_api'
4
+
5
+ interface ValidationErrorDetail {
6
+ operation: string
7
+ path: string
8
+ method: string
9
+ error: { message: string }
10
+ }
11
+
12
+ export function installValidationCapture(): () => void {
13
+ if (typeof window === 'undefined') return () => {}
14
+
15
+ const handler = (event: Event) => {
16
+ if (!(event instanceof CustomEvent)) return
17
+ try {
18
+ const detail = event.detail as ValidationErrorDetail
19
+ const { config } = monitorStore.getState()
20
+ monitorStore.getState().push({
21
+ event_type: EventType.WARNING,
22
+ level: EventLevel.WARN,
23
+ message: `Zod validation error in ${detail.operation}: ${detail.error?.message ?? 'unknown'}`,
24
+ url: window.location.href,
25
+ http_method: detail.method,
26
+ http_url: detail.path,
27
+ session_id: getSessionId(),
28
+ user_agent: navigator.userAgent,
29
+ project_name: config.project,
30
+ environment: config.environment,
31
+ extra: { operation: detail.operation, path: detail.path, method: detail.method },
32
+ })
33
+ } catch { /* never crash */ }
34
+ }
35
+
36
+ window.addEventListener('zod-validation-error', handler)
37
+ return () => window.removeEventListener('zod-validation-error', handler)
38
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @djangocfg/monitor/client
3
+ *
4
+ * Browser-side monitor. Use in 'use client' components only.
5
+ *
6
+ * @example
7
+ * ```tsx
8
+ * 'use client'
9
+ * import { useEffect } from 'react'
10
+ * import { FrontendMonitor } from '@djangocfg/monitor/client'
11
+ *
12
+ * export function MonitorProvider() {
13
+ * useEffect(() => {
14
+ * FrontendMonitor.init({ project: 'my-app', environment: process.env.NODE_ENV })
15
+ * return () => FrontendMonitor.destroy()
16
+ * }, [])
17
+ * return null
18
+ * }
19
+ * ```
20
+ */
21
+
22
+ import { installJsErrorCapture } from './capture/js-errors'
23
+ import { installConsoleCapture } from './capture/console'
24
+ import { installValidationCapture } from './capture/validation'
25
+ import { ensureSessionCookie } from './capture/session'
26
+ import { monitorStore } from './store'
27
+ import { configureMonitorApi } from '../_api'
28
+ import type { MonitorConfig, MonitorEvent } from '../types'
29
+
30
+ export type { MonitorConfig, MonitorEvent, EventType, EventLevel } from '../types'
31
+ export { monitoredFetch } from './capture/network'
32
+ export { getSessionId } from './capture/session'
33
+
34
+ let flushInterval: ReturnType<typeof setInterval> | null = null
35
+ const cleanupFns: Array<() => void> = []
36
+
37
+ export const FrontendMonitor = {
38
+ init(config: MonitorConfig = {}): void {
39
+ if (typeof window === 'undefined') return
40
+
41
+ if (config.baseUrl) configureMonitorApi(config.baseUrl)
42
+ monitorStore.getState().setConfig(config)
43
+ ensureSessionCookie()
44
+
45
+ if (config.captureJsErrors !== false) cleanupFns.push(installJsErrorCapture())
46
+ if (config.captureConsole !== false) cleanupFns.push(installConsoleCapture())
47
+ cleanupFns.push(installValidationCapture())
48
+
49
+ const interval = config.flushInterval ?? 5000
50
+ flushInterval = setInterval(() => monitorStore.getState().flush(), interval)
51
+
52
+ const onHide = () => { if (document.visibilityState === 'hidden') monitorStore.getState().flush(true) }
53
+ window.addEventListener('visibilitychange', onHide)
54
+ window.addEventListener('beforeunload', () => monitorStore.getState().flush(true))
55
+
56
+ if (config.debug) console.info('[FrontendMonitor] initialized', config)
57
+ },
58
+
59
+ capture(event: MonitorEvent): void {
60
+ monitorStore.getState().push(event)
61
+ },
62
+
63
+ flush(): void {
64
+ monitorStore.getState().flush()
65
+ },
66
+
67
+ destroy(): void {
68
+ if (flushInterval !== null) { clearInterval(flushInterval); flushInterval = null }
69
+ cleanupFns.forEach((fn) => fn())
70
+ cleanupFns.length = 0
71
+ },
72
+ }
@@ -0,0 +1,41 @@
1
+ import { createStore } from 'zustand/vanilla'
2
+ import type { MonitorEvent, MonitorConfig } from '../../types'
3
+ import { sendBatch } from '../transport/ingest'
4
+
5
+ interface MonitorState {
6
+ config: MonitorConfig
7
+ buffer: MonitorEvent[]
8
+ initialized: boolean
9
+ push: (event: MonitorEvent) => void
10
+ flush: (useBeacon?: boolean) => void
11
+ setConfig: (config: MonitorConfig) => void
12
+ }
13
+
14
+ export const monitorStore = createStore<MonitorState>((set, get) => ({
15
+ config: {},
16
+ buffer: [],
17
+ initialized: false,
18
+
19
+ push(event) {
20
+ const { config, buffer } = get()
21
+ const maxSize = config.maxBufferSize ?? 20
22
+ const next = [...buffer, event]
23
+ set({ buffer: next })
24
+
25
+ if (next.length >= maxSize || event.level === 'error') {
26
+ get().flush()
27
+ }
28
+ },
29
+
30
+ flush(useBeacon = false) {
31
+ const { buffer } = get()
32
+ if (buffer.length === 0) return
33
+ const batch = buffer.slice(0, 50)
34
+ set({ buffer: buffer.slice(50) })
35
+ sendBatch({ events: batch }, useBeacon)
36
+ },
37
+
38
+ setConfig(config) {
39
+ set({ config, initialized: true })
40
+ },
41
+ }))
@@ -0,0 +1,31 @@
1
+ import { monitorApi } from '../../_api'
2
+ import { API, MemoryStorageAdapter, KeepAliveFetchAdapter } from '../../_api/generated/cfg_monitor'
3
+ import type { IngestBatchRequest } from '../../_api'
4
+
5
+ /** Separate API instance with KeepAliveFetchAdapter — survives page unload */
6
+ const monitorApiBeacon = new API('', {
7
+ storage: new MemoryStorageAdapter(),
8
+ httpClient: new KeepAliveFetchAdapter(),
9
+ })
10
+
11
+ export function syncBeaconBaseUrl(): void {
12
+ monitorApiBeacon.setBaseUrl(monitorApi.getBaseUrl())
13
+ }
14
+
15
+ export async function sendBatch(
16
+ batch: IngestBatchRequest,
17
+ useBeacon = false,
18
+ ): Promise<void> {
19
+ if (batch.events.length === 0) return
20
+
21
+ try {
22
+ if (useBeacon) {
23
+ syncBeaconBaseUrl()
24
+ await monitorApiBeacon.monitor.ingestCreate(batch)
25
+ } else {
26
+ await monitorApi.monitor.ingestCreate(batch)
27
+ }
28
+ } catch {
29
+ // Transport errors silently ignored — monitoring must never crash the app
30
+ }
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @djangocfg/monitor
3
+ *
4
+ * Re-exports types only from the root entry point.
5
+ * Use sub-paths for the actual monitor instances:
6
+ *
7
+ * - `@djangocfg/monitor/client` — browser SDK (use in 'use client' components)
8
+ * - `@djangocfg/monitor/server` — server SDK (route handlers, Server Components)
9
+ */
10
+
11
+ export type { EventType, EventLevel, MonitorEvent } from './types'
12
+ export type { MonitorConfig, ServerMonitorConfig } from './types'
@@ -0,0 +1,85 @@
1
+ /**
2
+ * @djangocfg/monitor/server
3
+ *
4
+ * Server-side monitor for Next.js route handlers, Server Components,
5
+ * and middleware. Does NOT use browser APIs — safe in Node.js / Edge Runtime.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // lib/monitor.ts
10
+ * import { serverMonitor } from '@djangocfg/monitor/server'
11
+ * serverMonitor.configure({ project: 'my-app', environment: 'production', baseUrl: 'https://api.myapp.com' })
12
+ * export { serverMonitor }
13
+ *
14
+ * // app/api/orders/route.ts
15
+ * import { serverMonitor } from '@/lib/monitor'
16
+ * export async function POST(req: Request) {
17
+ * try { ... } catch (err) {
18
+ * await serverMonitor.captureError(err, { url: req.url })
19
+ * return new Response('Internal Server Error', { status: 500 })
20
+ * }
21
+ * }
22
+ * ```
23
+ */
24
+
25
+ import { monitorApi, configureMonitorApi, EventType, EventLevel } from '../_api'
26
+ import type { FrontendEventIngestRequest } from '../_api'
27
+ import type { ServerMonitorConfig } from '../types'
28
+
29
+ export type { ServerMonitorConfig } from '../types'
30
+ export type { EventType, EventLevel, MonitorEvent } from '../types'
31
+
32
+ let _config: ServerMonitorConfig = {}
33
+
34
+ async function send(events: FrontendEventIngestRequest[]): Promise<void> {
35
+ if (events.length === 0) return
36
+ try {
37
+ await monitorApi.monitor.ingestCreate({ events })
38
+ } catch {
39
+ // Never throw from monitor
40
+ }
41
+ }
42
+
43
+ function withDefaults(event: FrontendEventIngestRequest): FrontendEventIngestRequest {
44
+ return { project_name: _config.project, environment: _config.environment, ...event }
45
+ }
46
+
47
+ export const serverMonitor = {
48
+ configure(config: ServerMonitorConfig): void {
49
+ _config = config
50
+ if (config.baseUrl) configureMonitorApi(config.baseUrl)
51
+ },
52
+
53
+ async captureError(err: unknown, ctx?: { url?: string; extra?: Record<string, unknown> }): Promise<void> {
54
+ await send([withDefaults({
55
+ event_type: EventType.JS_ERROR,
56
+ level: EventLevel.ERROR,
57
+ message: err instanceof Error ? err.message : String(err),
58
+ stack_trace: err instanceof Error ? (err.stack ?? '') : '',
59
+ url: ctx?.url ?? '',
60
+ extra: ctx?.extra,
61
+ })])
62
+ },
63
+
64
+ async captureNetworkError(
65
+ status: number,
66
+ method: string,
67
+ apiUrl: string,
68
+ ctx?: { pageUrl?: string; extra?: Record<string, unknown> },
69
+ ): Promise<void> {
70
+ await send([withDefaults({
71
+ event_type: EventType.NETWORK_ERROR,
72
+ level: status >= 500 ? EventLevel.ERROR : EventLevel.WARN,
73
+ message: `HTTP ${status} — ${method} ${apiUrl}`,
74
+ url: ctx?.pageUrl ?? '',
75
+ http_status: status,
76
+ http_method: method,
77
+ http_url: apiUrl,
78
+ extra: ctx?.extra,
79
+ })])
80
+ },
81
+
82
+ async capture(event: FrontendEventIngestRequest): Promise<void> {
83
+ await send([withDefaults(event)])
84
+ },
85
+ }