@wippy-fe/log 0.0.19
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 +47 -0
- package/src/console-transport.ts +68 -0
- package/src/default-receiver.ts +27 -0
- package/src/gelf-transport.ts +93 -0
- package/src/index.ts +22 -0
- package/src/logger.ts +69 -0
- package/src/receiver-core.ts +150 -0
- package/src/sentry-transport.ts +112 -0
- package/src/types.ts +17 -0
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wippy-fe/log",
|
|
3
|
+
"version": "0.0.19",
|
|
4
|
+
"description": "Logging layer for Wippy micro-frontend platform",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"import": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.mts"
|
|
11
|
+
},
|
|
12
|
+
"./default-receiver": {
|
|
13
|
+
"import": "./dist/default-receiver.mjs",
|
|
14
|
+
"types": "./dist/default-receiver.d.mts"
|
|
15
|
+
},
|
|
16
|
+
"./logger": {
|
|
17
|
+
"import": "./dist/logger.mjs",
|
|
18
|
+
"types": "./dist/logger.d.mts"
|
|
19
|
+
},
|
|
20
|
+
"./sentry-transport": {
|
|
21
|
+
"import": "./dist/sentry-transport.mjs",
|
|
22
|
+
"types": "./dist/sentry-transport.d.mts"
|
|
23
|
+
},
|
|
24
|
+
"./console-transport": {
|
|
25
|
+
"import": "./dist/console-transport.mjs",
|
|
26
|
+
"types": "./dist/console-transport.d.mts"
|
|
27
|
+
},
|
|
28
|
+
"./gelf-transport": {
|
|
29
|
+
"import": "./dist/gelf-transport.mjs",
|
|
30
|
+
"types": "./dist/gelf-transport.d.mts"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"main": "dist/index.mjs",
|
|
34
|
+
"module": "dist/index.mjs",
|
|
35
|
+
"types": "dist/index.d.mts",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build:types": "tsup"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@sentry/browser": ">=8.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"@sentry/browser": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* eslint-disable no-console -- this IS the console transport */
|
|
2
|
+
import type { LogTransport, NormalizedLogEntry } from './types'
|
|
3
|
+
import { LogLevel, reconstructError } from './types'
|
|
4
|
+
|
|
5
|
+
export interface ConsoleTransportOptions {
|
|
6
|
+
collapsed?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatPrefix(entry: NormalizedLogEntry): string {
|
|
10
|
+
const { source } = entry
|
|
11
|
+
if (source.resourceId)
|
|
12
|
+
return `[${source.layer}:${source.resourceType ?? 'unknown'}:${source.resourceId}]`
|
|
13
|
+
return `[${source.layer}]`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function collectMetadata(entry: NormalizedLogEntry): Record<string, unknown> | null {
|
|
17
|
+
const meta: Record<string, unknown> = {}
|
|
18
|
+
if (entry.callerStack)
|
|
19
|
+
meta.callerStack = entry.callerStack
|
|
20
|
+
if (entry.extra)
|
|
21
|
+
meta.extra = entry.extra
|
|
22
|
+
if (Object.keys(entry.tags).length > 0)
|
|
23
|
+
meta.tags = entry.tags
|
|
24
|
+
if (Object.keys(entry.contexts).length > 0)
|
|
25
|
+
meta.contexts = entry.contexts
|
|
26
|
+
if (Object.keys(meta).length === 0)
|
|
27
|
+
return null
|
|
28
|
+
return meta
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function consoleTransport(options?: ConsoleTransportOptions): LogTransport {
|
|
32
|
+
return {
|
|
33
|
+
name: 'console',
|
|
34
|
+
handle(entry: NormalizedLogEntry) {
|
|
35
|
+
const prefix = formatPrefix(entry)
|
|
36
|
+
const message = `${prefix} ${entry.message}`
|
|
37
|
+
const meta = collectMetadata(entry)
|
|
38
|
+
|
|
39
|
+
if (entry.subAction === 'captureException' && entry.error) {
|
|
40
|
+
const err = reconstructError(entry.error)
|
|
41
|
+
|
|
42
|
+
if (options?.collapsed && entry.breadcrumbs.length > 0) {
|
|
43
|
+
console.groupCollapsed(message)
|
|
44
|
+
console.error(err)
|
|
45
|
+
if (meta)
|
|
46
|
+
console.log(meta)
|
|
47
|
+
console.log('Breadcrumbs:', entry.breadcrumbs)
|
|
48
|
+
console.groupEnd()
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
console.error(message, err, ...(meta ? [meta] : []))
|
|
52
|
+
}
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const args: unknown[] = meta ? [message, meta] : [message]
|
|
57
|
+
|
|
58
|
+
if (entry.level <= LogLevel.Error)
|
|
59
|
+
console.error(...args)
|
|
60
|
+
else if (entry.level <= LogLevel.Warning)
|
|
61
|
+
console.warn(...args)
|
|
62
|
+
else if (entry.level <= LogLevel.Info)
|
|
63
|
+
console.info(...args)
|
|
64
|
+
else
|
|
65
|
+
console.debug(...args)
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { LogReceiverHandle, LogTransport } from './types'
|
|
2
|
+
import { consoleTransport } from './console-transport'
|
|
3
|
+
import { createReceiverCore } from './receiver-core'
|
|
4
|
+
|
|
5
|
+
export function createDefaultLogReceiver(env: Record<string, string>): LogReceiverHandle {
|
|
6
|
+
const transports: LogTransport[] = [consoleTransport()]
|
|
7
|
+
|
|
8
|
+
if (env.PUBLIC_SENTRY_DSN) {
|
|
9
|
+
const dsn = env.PUBLIC_SENTRY_DSN
|
|
10
|
+
import('./sentry-transport').then(({ sentryTransport }) => {
|
|
11
|
+
transports.push(sentryTransport({ dsn }))
|
|
12
|
+
}).catch((e) => {
|
|
13
|
+
console.warn('[@wippy-fe/log] Sentry transport unavailable:', e)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (env.PUBLIC_GELF_URL) {
|
|
18
|
+
const url = env.PUBLIC_GELF_URL
|
|
19
|
+
import('./gelf-transport').then(({ gelfTransport }) => {
|
|
20
|
+
transports.push(gelfTransport({ url }))
|
|
21
|
+
}).catch((e) => {
|
|
22
|
+
console.warn('[@wippy-fe/log] GELF transport unavailable:', e)
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return createReceiverCore(transports)
|
|
27
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { LogTransport, NormalizedLogEntry } from './types'
|
|
2
|
+
|
|
3
|
+
export interface GelfTransportOptions {
|
|
4
|
+
url: string
|
|
5
|
+
host?: string
|
|
6
|
+
facility?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BATCH_INTERVAL = 1000
|
|
10
|
+
const MAX_BATCH_SIZE = 20
|
|
11
|
+
|
|
12
|
+
export function gelfTransport(options: GelfTransportOptions): LogTransport {
|
|
13
|
+
const { url, host = 'wippy', facility } = options
|
|
14
|
+
let batch: object[] = []
|
|
15
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
16
|
+
|
|
17
|
+
function sendBatch(): void {
|
|
18
|
+
if (batch.length === 0)
|
|
19
|
+
return
|
|
20
|
+
const entries = batch
|
|
21
|
+
batch = []
|
|
22
|
+
timer = null
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
fetch(url, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify(entry),
|
|
29
|
+
}).catch(() => {})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scheduleSend(): void {
|
|
34
|
+
if (!timer)
|
|
35
|
+
timer = setTimeout(sendBatch, BATCH_INTERVAL)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toGelf(entry: NormalizedLogEntry): object {
|
|
39
|
+
const gelf: Record<string, unknown> = {
|
|
40
|
+
version: '1.1',
|
|
41
|
+
host,
|
|
42
|
+
short_message: entry.message,
|
|
43
|
+
timestamp: entry.timestamp / 1000,
|
|
44
|
+
level: entry.level,
|
|
45
|
+
_wippy_layer: entry.source.layer,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (facility)
|
|
49
|
+
gelf.facility = facility
|
|
50
|
+
if (entry.source.resourceId)
|
|
51
|
+
gelf._wippy_resourceId = entry.source.resourceId
|
|
52
|
+
if (entry.source.resourceType)
|
|
53
|
+
gelf._wippy_resourceType = entry.source.resourceType
|
|
54
|
+
if (entry.source.nestingDepth != null)
|
|
55
|
+
gelf._wippy_nestingDepth = entry.source.nestingDepth
|
|
56
|
+
|
|
57
|
+
if (entry.error?.stack)
|
|
58
|
+
gelf.full_message = entry.error.stack
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(entry.tags))
|
|
61
|
+
gelf[`_tag_${key}`] = value
|
|
62
|
+
|
|
63
|
+
if (entry.extra) {
|
|
64
|
+
for (const [key, value] of Object.entries(entry.extra))
|
|
65
|
+
gelf[`_extra_${key}`] = typeof value === 'string' ? value : JSON.stringify(value)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return gelf
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name: 'gelf',
|
|
73
|
+
handle(entry: NormalizedLogEntry) {
|
|
74
|
+
batch.push(toGelf(entry))
|
|
75
|
+
if (batch.length >= MAX_BATCH_SIZE)
|
|
76
|
+
sendBatch()
|
|
77
|
+
else
|
|
78
|
+
scheduleSend()
|
|
79
|
+
},
|
|
80
|
+
async flush() {
|
|
81
|
+
if (timer) {
|
|
82
|
+
clearTimeout(timer)
|
|
83
|
+
timer = null
|
|
84
|
+
}
|
|
85
|
+
sendBatch()
|
|
86
|
+
},
|
|
87
|
+
destroy() {
|
|
88
|
+
if (timer)
|
|
89
|
+
clearTimeout(timer)
|
|
90
|
+
batch = []
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LogReceiverHandle, LogReceiverOptions } from './types'
|
|
2
|
+
import { createReceiverCore } from './receiver-core'
|
|
3
|
+
|
|
4
|
+
export type {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
CmdLogPayload,
|
|
7
|
+
CmdLogSubAction,
|
|
8
|
+
CreateLoggerOptions,
|
|
9
|
+
LoggerApi,
|
|
10
|
+
LogLevelValue,
|
|
11
|
+
LogReceiverHandle,
|
|
12
|
+
LogReceiverOptions,
|
|
13
|
+
LogSource,
|
|
14
|
+
LogTransport,
|
|
15
|
+
NormalizedLogEntry,
|
|
16
|
+
} from './types'
|
|
17
|
+
|
|
18
|
+
export { LogLevel } from './types'
|
|
19
|
+
|
|
20
|
+
export function createLogReceiver(options: LogReceiverOptions): LogReceiverHandle {
|
|
21
|
+
return createReceiverCore(options.transports, options.context)
|
|
22
|
+
}
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CmdLogSubAction, CreateLoggerOptions, LoggerApi, LogLevelValue, LogSource } from './types'
|
|
2
|
+
import { captureCallerStack, LogLevel, serializeError } from './types'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MESSAGE_TYPE = '@gen2-chat'
|
|
5
|
+
|
|
6
|
+
// Frames: caller → info/warn/error → logWithStack → send
|
|
7
|
+
const LOG_FRAMES_TO_SKIP = 3
|
|
8
|
+
// Frames: caller → captureException/captureMessage → send
|
|
9
|
+
const CAPTURE_FRAMES_TO_SKIP = 2
|
|
10
|
+
|
|
11
|
+
export function createLogger(options: CreateLoggerOptions): LoggerApi {
|
|
12
|
+
const { source, messageType = DEFAULT_MESSAGE_TYPE } = options
|
|
13
|
+
const target = options.target ?? window.parent
|
|
14
|
+
|
|
15
|
+
function send(subAction: CmdLogSubAction, data: Record<string, unknown>, callerStack?: string): void {
|
|
16
|
+
target?.postMessage(JSON.stringify({
|
|
17
|
+
type: messageType,
|
|
18
|
+
action: 'cmd-log',
|
|
19
|
+
subAction,
|
|
20
|
+
data,
|
|
21
|
+
source,
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
callerStack,
|
|
24
|
+
}), '*')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function log(level: LogLevelValue, message: string, data?: Record<string, unknown>): void {
|
|
28
|
+
send('log', { message, level, extra: data })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function logWithStack(level: LogLevelValue, message: string, data?: Record<string, unknown>): void {
|
|
32
|
+
send('log', { message, level, extra: data }, captureCallerStack(LOG_FRAMES_TO_SKIP))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
debug: (message, data) => log(LogLevel.Debug, message, data),
|
|
37
|
+
info: (message, data) => logWithStack(LogLevel.Info, message, data),
|
|
38
|
+
warn: (message, data) => logWithStack(LogLevel.Warning, message, data),
|
|
39
|
+
error: (message, data) => logWithStack(LogLevel.Error, message, data),
|
|
40
|
+
|
|
41
|
+
captureException(error, extra) {
|
|
42
|
+
send('captureException', { ...serializeError(error), extra }, captureCallerStack(CAPTURE_FRAMES_TO_SKIP))
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
captureMessage(message, level, extra) {
|
|
46
|
+
send('captureMessage', { message, level: level ?? LogLevel.Info, extra }, captureCallerStack(CAPTURE_FRAMES_TO_SKIP))
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
addBreadcrumb(breadcrumb) {
|
|
50
|
+
send('addBreadcrumb', { ...breadcrumb, timestamp: breadcrumb.timestamp ?? Date.now() })
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
setContext(name, data) {
|
|
54
|
+
send('setContext', { name, data })
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
setTag(key, value) {
|
|
58
|
+
send('setTag', { key, value })
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createChildLogger(source: Omit<LogSource, 'layer'>): LoggerApi {
|
|
64
|
+
return createLogger({ source: { layer: 'child', ...source } })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createHostLogger(): LoggerApi {
|
|
68
|
+
return createLogger({ source: { layer: 'host' } })
|
|
69
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { Breadcrumb, LogReceiverHandle, LogSource, LogTransport, NormalizedLogEntry } from './types'
|
|
2
|
+
import { LogLevel } from './types'
|
|
3
|
+
|
|
4
|
+
interface SourceState {
|
|
5
|
+
breadcrumbs: Breadcrumb[]
|
|
6
|
+
contexts: Record<string, Record<string, unknown>>
|
|
7
|
+
tags: Record<string, string>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_BREADCRUMBS = 100
|
|
11
|
+
const DEFAULT_MESSAGE_TYPE = '@gen2-chat'
|
|
12
|
+
|
|
13
|
+
function getSourceKey(source: LogSource): string {
|
|
14
|
+
return `${source.layer}:${source.resourceId ?? 'unknown'}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isValidSource(source: unknown): source is LogSource {
|
|
18
|
+
return source != null
|
|
19
|
+
&& typeof source === 'object'
|
|
20
|
+
&& 'layer' in source
|
|
21
|
+
&& (source.layer === 'child' || source.layer === 'host')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createReceiverCore(
|
|
25
|
+
transports: LogTransport[],
|
|
26
|
+
globalContext?: Record<string, unknown>,
|
|
27
|
+
messageType: string = DEFAULT_MESSAGE_TYPE,
|
|
28
|
+
): LogReceiverHandle {
|
|
29
|
+
const sourceStates = new Map<string, SourceState>()
|
|
30
|
+
|
|
31
|
+
function getState(sourceKey: string): SourceState {
|
|
32
|
+
let state = sourceStates.get(sourceKey)
|
|
33
|
+
if (!state) {
|
|
34
|
+
state = { breadcrumbs: [], contexts: {}, tags: {} }
|
|
35
|
+
sourceStates.set(sourceKey, state)
|
|
36
|
+
}
|
|
37
|
+
return state
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dispatch(entry: NormalizedLogEntry): void {
|
|
41
|
+
for (const transport of transports)
|
|
42
|
+
transport.handle(entry)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleMessage(event: MessageEvent): void {
|
|
46
|
+
if (typeof event.data !== 'string')
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
let msg: any
|
|
50
|
+
try {
|
|
51
|
+
msg = JSON.parse(event.data)
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (msg.type !== messageType || msg.action !== 'cmd-log')
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
const { subAction, data, source, timestamp, callerStack } = msg
|
|
61
|
+
if (!data || typeof data !== 'object')
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
const resolvedSource: LogSource = isValidSource(source)
|
|
65
|
+
? source
|
|
66
|
+
: { layer: 'child' }
|
|
67
|
+
const sourceKey = getSourceKey(resolvedSource)
|
|
68
|
+
const state = getState(sourceKey)
|
|
69
|
+
|
|
70
|
+
switch (subAction) {
|
|
71
|
+
case 'addBreadcrumb': {
|
|
72
|
+
state.breadcrumbs.push(data as Breadcrumb)
|
|
73
|
+
if (state.breadcrumbs.length > MAX_BREADCRUMBS)
|
|
74
|
+
state.breadcrumbs.shift()
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
case 'setContext': {
|
|
78
|
+
const name = data.name as string | undefined
|
|
79
|
+
if (!name)
|
|
80
|
+
break
|
|
81
|
+
if (data.data === null)
|
|
82
|
+
delete state.contexts[name]
|
|
83
|
+
else
|
|
84
|
+
state.contexts[name] = data.data as Record<string, unknown>
|
|
85
|
+
break
|
|
86
|
+
}
|
|
87
|
+
case 'setTag': {
|
|
88
|
+
const key = data.key as string | undefined
|
|
89
|
+
if (!key)
|
|
90
|
+
break
|
|
91
|
+
if (data.value === null)
|
|
92
|
+
delete state.tags[key]
|
|
93
|
+
else
|
|
94
|
+
state.tags[key] = data.value as string
|
|
95
|
+
break
|
|
96
|
+
}
|
|
97
|
+
case 'log':
|
|
98
|
+
case 'captureException':
|
|
99
|
+
case 'captureMessage': {
|
|
100
|
+
const entry: NormalizedLogEntry = {
|
|
101
|
+
subAction,
|
|
102
|
+
level: data.level ?? LogLevel.Info,
|
|
103
|
+
message: data.message ?? '',
|
|
104
|
+
timestamp: timestamp ?? Date.now(),
|
|
105
|
+
source: resolvedSource,
|
|
106
|
+
breadcrumbs: [...state.breadcrumbs],
|
|
107
|
+
contexts: {
|
|
108
|
+
...(globalContext ? { global: globalContext } : {}),
|
|
109
|
+
...state.contexts,
|
|
110
|
+
},
|
|
111
|
+
tags: { ...state.tags },
|
|
112
|
+
}
|
|
113
|
+
if (callerStack)
|
|
114
|
+
entry.callerStack = callerStack
|
|
115
|
+
if (data.extra)
|
|
116
|
+
entry.extra = data.extra as Record<string, unknown>
|
|
117
|
+
if (subAction === 'captureException') {
|
|
118
|
+
entry.error = {
|
|
119
|
+
name: (data.name as string) ?? 'Error',
|
|
120
|
+
message: (data.message as string) ?? '',
|
|
121
|
+
stack: data.stack as string | undefined,
|
|
122
|
+
}
|
|
123
|
+
entry.level = data.level ?? LogLevel.Error
|
|
124
|
+
}
|
|
125
|
+
dispatch(entry)
|
|
126
|
+
break
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
window.addEventListener('message', handleMessage)
|
|
132
|
+
|
|
133
|
+
function onUnload(): void {
|
|
134
|
+
for (const transport of transports)
|
|
135
|
+
transport.flush?.()
|
|
136
|
+
}
|
|
137
|
+
window.addEventListener('beforeunload', onUnload)
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
destroy() {
|
|
141
|
+
window.removeEventListener('message', handleMessage)
|
|
142
|
+
window.removeEventListener('beforeunload', onUnload)
|
|
143
|
+
for (const transport of transports) {
|
|
144
|
+
transport.flush?.()
|
|
145
|
+
transport.destroy?.()
|
|
146
|
+
}
|
|
147
|
+
sourceStates.clear()
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { LogTransport, NormalizedLogEntry } from './types'
|
|
2
|
+
import { LogLevel, reconstructError } from './types'
|
|
3
|
+
|
|
4
|
+
export interface SentryTransportOptions {
|
|
5
|
+
dsn: string
|
|
6
|
+
environment?: string
|
|
7
|
+
release?: string
|
|
8
|
+
tracesSampleRate?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type SentryModule = typeof import('@sentry/browser')
|
|
12
|
+
|
|
13
|
+
let sentryInitialized = false
|
|
14
|
+
let Sentry: SentryModule | null = null
|
|
15
|
+
|
|
16
|
+
async function ensureSentry(options: SentryTransportOptions): Promise<SentryModule | null> {
|
|
17
|
+
if (sentryInitialized)
|
|
18
|
+
return Sentry
|
|
19
|
+
try {
|
|
20
|
+
Sentry = await import('@sentry/browser')
|
|
21
|
+
Sentry.init({
|
|
22
|
+
dsn: options.dsn,
|
|
23
|
+
environment: options.environment,
|
|
24
|
+
release: options.release,
|
|
25
|
+
tracesSampleRate: options.tracesSampleRate ?? 0,
|
|
26
|
+
})
|
|
27
|
+
sentryInitialized = true
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
console.warn('[@wippy-fe/log] Failed to load @sentry/browser:', e)
|
|
31
|
+
}
|
|
32
|
+
return Sentry
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toSentryLevel(level: number): 'fatal' | 'error' | 'warning' | 'info' | 'debug' {
|
|
36
|
+
if (level <= LogLevel.Alert)
|
|
37
|
+
return 'fatal'
|
|
38
|
+
if (level <= LogLevel.Error)
|
|
39
|
+
return 'error'
|
|
40
|
+
if (level <= LogLevel.Warning)
|
|
41
|
+
return 'warning'
|
|
42
|
+
if (level <= LogLevel.Info)
|
|
43
|
+
return 'info'
|
|
44
|
+
return 'debug'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function sentryTransport(options: SentryTransportOptions): LogTransport {
|
|
48
|
+
const ready = ensureSentry(options)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
name: 'sentry',
|
|
52
|
+
handle(entry: NormalizedLogEntry) {
|
|
53
|
+
ready.then((sentry) => {
|
|
54
|
+
if (!sentry)
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
sentry.withScope((scope) => {
|
|
58
|
+
scope.setTag('wippy.layer', entry.source.layer)
|
|
59
|
+
if (entry.source.resourceId)
|
|
60
|
+
scope.setTag('wippy.resourceId', entry.source.resourceId)
|
|
61
|
+
if (entry.source.resourceType)
|
|
62
|
+
scope.setTag('wippy.resourceType', entry.source.resourceType)
|
|
63
|
+
if (entry.source.nestingDepth != null)
|
|
64
|
+
scope.setTag('wippy.nestingDepth', String(entry.source.nestingDepth))
|
|
65
|
+
|
|
66
|
+
for (const [key, value] of Object.entries(entry.tags))
|
|
67
|
+
scope.setTag(key, value)
|
|
68
|
+
|
|
69
|
+
scope.setContext('wippy', { ...entry.source })
|
|
70
|
+
for (const [name, data] of Object.entries(entry.contexts))
|
|
71
|
+
scope.setContext(name, data)
|
|
72
|
+
|
|
73
|
+
for (const bc of entry.breadcrumbs) {
|
|
74
|
+
sentry.addBreadcrumb({
|
|
75
|
+
category: bc.category,
|
|
76
|
+
message: bc.message,
|
|
77
|
+
data: bc.data,
|
|
78
|
+
level: toSentryLevel(bc.level ?? LogLevel.Info),
|
|
79
|
+
type: bc.type,
|
|
80
|
+
timestamp: bc.timestamp ? bc.timestamp / 1000 : undefined,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (entry.extra)
|
|
85
|
+
scope.setExtras(entry.extra)
|
|
86
|
+
|
|
87
|
+
if (entry.callerStack)
|
|
88
|
+
scope.setExtra('callerStack', entry.callerStack)
|
|
89
|
+
|
|
90
|
+
if (entry.subAction === 'captureException' && entry.error) {
|
|
91
|
+
const err = reconstructError(entry.error)
|
|
92
|
+
if (!err.stack && entry.callerStack)
|
|
93
|
+
err.stack = entry.callerStack
|
|
94
|
+
sentry.captureException(err)
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
sentry.captureMessage(entry.message, toSentryLevel(entry.level))
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
},
|
|
102
|
+
async flush() {
|
|
103
|
+
const sentry = await ready
|
|
104
|
+
if (sentry)
|
|
105
|
+
await sentry.flush(2000)
|
|
106
|
+
},
|
|
107
|
+
destroy() {
|
|
108
|
+
if (Sentry)
|
|
109
|
+
Sentry.close()
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
type Breadcrumb,
|
|
3
|
+
captureCallerStack,
|
|
4
|
+
type CmdLogPayload,
|
|
5
|
+
type CmdLogSubAction,
|
|
6
|
+
type CreateLoggerOptions,
|
|
7
|
+
type LoggerApi,
|
|
8
|
+
LogLevel,
|
|
9
|
+
type LogLevelValue,
|
|
10
|
+
type LogReceiverHandle,
|
|
11
|
+
type LogReceiverOptions,
|
|
12
|
+
type LogSource,
|
|
13
|
+
type LogTransport,
|
|
14
|
+
type NormalizedLogEntry,
|
|
15
|
+
reconstructError,
|
|
16
|
+
serializeError,
|
|
17
|
+
} from '../../../src/shared/logging/types'
|