@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.
- package/README.md +341 -0
- package/dist/client.cjs +1273 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +123 -0
- package/dist/client.d.ts +123 -0
- package/dist/client.mjs +1243 -0
- package/dist/client.mjs.map +1 -0
- package/dist/index.cjs +18 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +101 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -0
- package/dist/server.cjs +947 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +117 -0
- package/dist/server.d.ts +117 -0
- package/dist/server.mjs +917 -0
- package/dist/server.mjs.map +1 -0
- package/package.json +82 -0
- package/src/.claude/.sidecar/activity.jsonl +1 -0
- package/src/.claude/.sidecar/map_cache.json +38 -0
- package/src/.claude/.sidecar/usage.json +5 -0
- package/src/.claude/project-map.md +29 -0
- package/src/_api/BaseClient.ts +18 -0
- package/src/_api/generated/cfg_monitor/CLAUDE.md +60 -0
- package/src/_api/generated/cfg_monitor/_utils/fetchers/index.ts +30 -0
- package/src/_api/generated/cfg_monitor/_utils/fetchers/monitor.ts +51 -0
- package/src/_api/generated/cfg_monitor/_utils/hooks/index.ts +30 -0
- package/src/_api/generated/cfg_monitor/_utils/hooks/monitor.ts +43 -0
- package/src/_api/generated/cfg_monitor/_utils/schemas/FrontendEventIngestRequest.schema.ts +34 -0
- package/src/_api/generated/cfg_monitor/_utils/schemas/IngestBatchRequest.schema.ts +20 -0
- package/src/_api/generated/cfg_monitor/_utils/schemas/index.ts +22 -0
- package/src/_api/generated/cfg_monitor/api-instance.ts +181 -0
- package/src/_api/generated/cfg_monitor/client.ts +322 -0
- package/src/_api/generated/cfg_monitor/enums.ts +36 -0
- package/src/_api/generated/cfg_monitor/errors.ts +118 -0
- package/src/_api/generated/cfg_monitor/http.ts +137 -0
- package/src/_api/generated/cfg_monitor/index.ts +317 -0
- package/src/_api/generated/cfg_monitor/logger.ts +261 -0
- package/src/_api/generated/cfg_monitor/monitor/client.ts +25 -0
- package/src/_api/generated/cfg_monitor/monitor/index.ts +4 -0
- package/src/_api/generated/cfg_monitor/monitor/models.ts +48 -0
- package/src/_api/generated/cfg_monitor/retry.ts +177 -0
- package/src/_api/generated/cfg_monitor/schema.json +184 -0
- package/src/_api/generated/cfg_monitor/storage.ts +163 -0
- package/src/_api/generated/cfg_monitor/validation-events.ts +135 -0
- package/src/_api/index.ts +6 -0
- package/src/client/capture/console.ts +72 -0
- package/src/client/capture/fingerprint.ts +27 -0
- package/src/client/capture/js-errors.ts +70 -0
- package/src/client/capture/network.ts +47 -0
- package/src/client/capture/session.ts +33 -0
- package/src/client/capture/validation.ts +38 -0
- package/src/client/index.ts +72 -0
- package/src/client/store/index.ts +41 -0
- package/src/client/transport/ingest.ts +31 -0
- package/src/index.ts +12 -0
- package/src/server/index.ts +85 -0
- package/src/types/config.ts +33 -0
- package/src/types/events.ts +5 -0
- 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
|
+
}
|