@codeleap/logger 4.3.0
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 +31 -0
- package/package.json.bak +31 -0
- package/src/Analytics.ts +67 -0
- package/src/Logger.ts +223 -0
- package/src/Sentry.ts +54 -0
- package/src/Slack.ts +138 -0
- package/src/constants.ts +37 -0
- package/src/index.ts +5 -0
- package/src/obfuscate.ts +76 -0
- package/src/performance/errors.ts +26 -0
- package/src/performance/index.ts +86 -0
- package/src/silentLogger.ts +11 -0
- package/src/types.ts +45 -0
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/logger",
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/logger"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@codeleap/types": "4.3.0",
|
|
13
|
+
"@codeleap/utils": "4.3.0",
|
|
14
|
+
"@codeleap/config": "4.3.0",
|
|
15
|
+
"ts-node-dev": "1.1.8",
|
|
16
|
+
"@sentry/types": "8.40.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "echo 'No build needed'"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@codeleap/types": "4.3.0",
|
|
23
|
+
"@codeleap/utils": "4.3.0",
|
|
24
|
+
"typescript": "5.0.4",
|
|
25
|
+
"react": "18.1.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"util": "0.12.5",
|
|
29
|
+
"url-parse": "^1.5.10"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/package.json.bak
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/logger",
|
|
3
|
+
"version": "4.3.0",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/logger"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@codeleap/types": "workspace:*",
|
|
13
|
+
"@codeleap/utils": "workspace:*",
|
|
14
|
+
"@codeleap/config": "workspace:*",
|
|
15
|
+
"ts-node-dev": "1.1.8",
|
|
16
|
+
"@sentry/types": "8.40.0"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "echo 'No build needed'"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@codeleap/types": "workspace:*",
|
|
23
|
+
"@codeleap/utils": "workspace:*",
|
|
24
|
+
"typescript": "5.0.4",
|
|
25
|
+
"react": "18.1.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"util": "0.12.5",
|
|
29
|
+
"url-parse": "^1.5.10"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/Analytics.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { obfuscate } from './obfuscate'
|
|
2
|
+
import { FunctionType, AnyFunction, AppSettings } from '@codeleap/types'
|
|
3
|
+
|
|
4
|
+
export type AnalyticsObject = {
|
|
5
|
+
name: string
|
|
6
|
+
type: 'interaction' | 'event'
|
|
7
|
+
data: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type IAnalyticsArgs = {
|
|
11
|
+
init(): any
|
|
12
|
+
prepareData: () => any
|
|
13
|
+
error?: (err: any) => any
|
|
14
|
+
} & Record<`on${Capitalize<AnalyticsObject['type']>}`, FunctionType<[AnalyticsObject], void>>
|
|
15
|
+
|
|
16
|
+
export class Analytics {
|
|
17
|
+
|
|
18
|
+
constructor(private callers: IAnalyticsArgs, private settings: AppSettings) {
|
|
19
|
+
this.callers.init()
|
|
20
|
+
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private prepare() {
|
|
24
|
+
const data = this.callers.prepareData()
|
|
25
|
+
|
|
26
|
+
return data
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
obfuscate(data) {
|
|
30
|
+
return obfuscate({
|
|
31
|
+
object: data,
|
|
32
|
+
keys: this?.settings?.Logger?.Obfuscate?.keys || [],
|
|
33
|
+
values: this?.settings?.Logger?.Obfuscate?.values || [],
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
event(name: string, data = {}) {
|
|
38
|
+
this.handle(name, data, 'event', this.callers.onEvent)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interaction(name: string, data = {}) {
|
|
42
|
+
this.handle(name, data, 'interaction', this.callers.onInteraction)
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
onError(cb) {
|
|
47
|
+
this.callers.error = cb
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private handle(name: string, data: any, type: AnalyticsObject['type'], fn: AnyFunction) {
|
|
51
|
+
try {
|
|
52
|
+
|
|
53
|
+
const obfuscated = this.obfuscate({
|
|
54
|
+
...data,
|
|
55
|
+
...this.prepare(),
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
fn({
|
|
59
|
+
name,
|
|
60
|
+
type,
|
|
61
|
+
data: obfuscated,
|
|
62
|
+
})
|
|
63
|
+
} catch (e) {
|
|
64
|
+
this.callers.error(e)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/Logger.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { inspect } from 'util'
|
|
2
|
+
import { TypeGuards, LogType, AppSettings } from '@codeleap/types'
|
|
3
|
+
import { Analytics } from './Analytics'
|
|
4
|
+
import { SentryService } from './Sentry'
|
|
5
|
+
import { SlackService } from './Slack'
|
|
6
|
+
import { LogToTerminal, LogFunctionArgs, LogToTerminalArgs, LoggerMiddleware } from './types'
|
|
7
|
+
|
|
8
|
+
const logLevels: LogType[] = ['debug', 'info', 'log', 'warn', 'error']
|
|
9
|
+
|
|
10
|
+
const emptyFunction = () => { }
|
|
11
|
+
|
|
12
|
+
const hollowAnalytics = new Analytics({
|
|
13
|
+
init: emptyFunction,
|
|
14
|
+
onEvent: emptyFunction,
|
|
15
|
+
onInteraction: emptyFunction,
|
|
16
|
+
prepareData: () => ({}),
|
|
17
|
+
}, {})
|
|
18
|
+
|
|
19
|
+
export class Logger {
|
|
20
|
+
static settings: AppSettings
|
|
21
|
+
|
|
22
|
+
settings: AppSettings
|
|
23
|
+
|
|
24
|
+
sentry: SentryService
|
|
25
|
+
|
|
26
|
+
slack: SlackService
|
|
27
|
+
|
|
28
|
+
middleware: LoggerMiddleware[] = []
|
|
29
|
+
|
|
30
|
+
constructor(settings: AppSettings, middleware?: LoggerMiddleware[], public analytics?: Analytics) {
|
|
31
|
+
this.settings = settings
|
|
32
|
+
this.middleware = middleware || []
|
|
33
|
+
if (settings.Logger.isMain) {
|
|
34
|
+
Logger.settings = settings
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (settings?.Logger?.IgnoreWarnings?.length) {
|
|
38
|
+
const newConsole = (args, oldConsole) => {
|
|
39
|
+
const shouldIgnore = typeof args[0] === 'string' &&
|
|
40
|
+
settings.Logger.IgnoreWarnings.some(ignoredWarning => args.join(' ').includes(ignoredWarning))
|
|
41
|
+
if (shouldIgnore) return
|
|
42
|
+
else return oldConsole.apply(console, args)
|
|
43
|
+
}
|
|
44
|
+
const consoles = ['log', 'warn', 'error']
|
|
45
|
+
|
|
46
|
+
consoles.forEach(t => {
|
|
47
|
+
const tmp = console[t]
|
|
48
|
+
console[t] = (...args) => newConsole(args, tmp)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.sentry = new SentryService(settings)
|
|
53
|
+
|
|
54
|
+
this.slack = new SlackService(settings)
|
|
55
|
+
|
|
56
|
+
if (!analytics) {
|
|
57
|
+
this.analytics = hollowAnalytics
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.analytics.onError((err) => {
|
|
61
|
+
this.logToTerminal({
|
|
62
|
+
logType: 'error',
|
|
63
|
+
args: ['Error on analytics event', err, 'Internal'],
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static formatContent(logArgs: LogToTerminalArgs) {
|
|
70
|
+
const { logType, args: content, deviceIdentifier: deviceId, stringify, logKeys = true } = logArgs
|
|
71
|
+
|
|
72
|
+
const [descriptionOrValue, value, category] = content
|
|
73
|
+
|
|
74
|
+
const nArgs = content.length
|
|
75
|
+
let logContent = content
|
|
76
|
+
|
|
77
|
+
const logValue = nArgs === 1 ? descriptionOrValue : value
|
|
78
|
+
|
|
79
|
+
const shouldStringify = stringify && !!logValue && TypeGuards.isObject(logValue) && !(logValue instanceof Error)
|
|
80
|
+
const inspectOptions = Logger?.settings?.Logger?.inspect || {}
|
|
81
|
+
|
|
82
|
+
const displayValue = shouldStringify ? inspect(logValue, {
|
|
83
|
+
depth: 5,
|
|
84
|
+
showHidden: true,
|
|
85
|
+
...inspectOptions,
|
|
86
|
+
}) : logValue
|
|
87
|
+
|
|
88
|
+
if (nArgs === 3) {
|
|
89
|
+
logContent = [
|
|
90
|
+
`(${category}) ${descriptionOrValue}${displayValue ? ' ->' : ''}`,
|
|
91
|
+
displayValue,
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (nArgs === 2) {
|
|
96
|
+
logContent = [
|
|
97
|
+
`${descriptionOrValue}${displayValue ? ' ->' : ''}`,
|
|
98
|
+
displayValue,
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (nArgs === 1) {
|
|
103
|
+
const isObj = typeof descriptionOrValue === 'object' && !(descriptionOrValue instanceof Error)
|
|
104
|
+
const keys = isObj ? Object.keys(descriptionOrValue) : null
|
|
105
|
+
const title = isObj && keys.length && logKeys ?
|
|
106
|
+
`${keys.filter(i => !!i).slice(0, 3).join(', ')}${keys.length > 3 ? '...' : ''} ->`
|
|
107
|
+
: null
|
|
108
|
+
|
|
109
|
+
if (title) {
|
|
110
|
+
logContent = [
|
|
111
|
+
title,
|
|
112
|
+
displayValue,
|
|
113
|
+
]
|
|
114
|
+
} else {
|
|
115
|
+
logContent = [
|
|
116
|
+
displayValue,
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return logContent
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static coloredLog: LogToTerminal = (logArgs) => {
|
|
125
|
+
const { logType, args: content, deviceIdentifier: deviceId, stringify } = logArgs
|
|
126
|
+
|
|
127
|
+
const logContent = Logger.formatContent(logArgs)
|
|
128
|
+
|
|
129
|
+
const displayLog = logType === 'error' ? 'warn' : logType
|
|
130
|
+
|
|
131
|
+
console[displayLog](deviceId, ...logContent)
|
|
132
|
+
|
|
133
|
+
return logContent
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private logToTerminal: LogToTerminal = (logArgs) => {
|
|
137
|
+
const { logType, args, color } = logArgs
|
|
138
|
+
|
|
139
|
+
if (this.settings.Logger.Level === 'silent') return
|
|
140
|
+
|
|
141
|
+
const shouldLog = TypeGuards.isString(this.settings.Logger.Level) ?
|
|
142
|
+
logLevels.indexOf(logType) >=
|
|
143
|
+
logLevels.indexOf(this.settings.Logger.Level) : this.settings.Logger.Level.includes(logType)
|
|
144
|
+
if (!shouldLog) return
|
|
145
|
+
|
|
146
|
+
const content = Logger.formatContent(logArgs)
|
|
147
|
+
|
|
148
|
+
if (this.settings.Environment.IsDev) {
|
|
149
|
+
|
|
150
|
+
const deviceId = this.settings.Logger?.DeviceIdentifier ?
|
|
151
|
+
`[${this.settings.Logger.DeviceIdentifier}]` : ''
|
|
152
|
+
|
|
153
|
+
const stringify = this.settings.Logger?.StringifyObjects
|
|
154
|
+
|
|
155
|
+
this.middleware.forEach(m => m(logArgs, content))
|
|
156
|
+
|
|
157
|
+
Logger.coloredLog(
|
|
158
|
+
{
|
|
159
|
+
logType: logType as LogType,
|
|
160
|
+
args,
|
|
161
|
+
color,
|
|
162
|
+
deviceIdentifier: deviceId,
|
|
163
|
+
stringify,
|
|
164
|
+
},
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!this.settings.Environment.IsDev || this.settings.Logger.alwaysSendToSentry) {
|
|
170
|
+
try {
|
|
171
|
+
|
|
172
|
+
this.middleware.forEach(m => m(logArgs, content))
|
|
173
|
+
if (['info', 'log'].includes(logType)) {
|
|
174
|
+
|
|
175
|
+
this.sentry.captureBreadcrumb(
|
|
176
|
+
logType,
|
|
177
|
+
content,
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
if (['error'].includes(logType)) {
|
|
181
|
+
this.sentry.sendLog(args?.[1] || args?.[0])
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
// Nothing
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
info(...args: LogFunctionArgs) {
|
|
190
|
+
this.logToTerminal({
|
|
191
|
+
args,
|
|
192
|
+
logType: 'info',
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
error(...args: LogFunctionArgs) {
|
|
197
|
+
this.logToTerminal({
|
|
198
|
+
args,
|
|
199
|
+
logType: 'error',
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
warn(...args: LogFunctionArgs) {
|
|
204
|
+
this.logToTerminal({
|
|
205
|
+
args,
|
|
206
|
+
logType: 'warn',
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
log(...args: LogFunctionArgs) {
|
|
211
|
+
this.logToTerminal({
|
|
212
|
+
args,
|
|
213
|
+
logType: 'log',
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
debug(...args: LogFunctionArgs) {
|
|
218
|
+
this.logToTerminal({
|
|
219
|
+
args,
|
|
220
|
+
logType: 'debug',
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/Sentry.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Breadcrumb, ClientOptions } from '@sentry/types'
|
|
2
|
+
import { AppSettings } from '@codeleap/types'
|
|
3
|
+
import {
|
|
4
|
+
LogFunctionArgs,
|
|
5
|
+
LogType,
|
|
6
|
+
SentrySeverityMap,
|
|
7
|
+
SentryProvider,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
export class SentryService {
|
|
11
|
+
private sentry: SentryProvider
|
|
12
|
+
|
|
13
|
+
private use: boolean
|
|
14
|
+
|
|
15
|
+
constructor(settings: AppSettings) {
|
|
16
|
+
this.use = settings?.Sentry?.enable
|
|
17
|
+
this.sentry = settings?.Sentry?.provider as SentryProvider
|
|
18
|
+
if (this.use) {
|
|
19
|
+
const isDebug = settings?.Sentry?.debug || false
|
|
20
|
+
if (isDebug) console.log('> > > Initializing Sentry', settings.Sentry)
|
|
21
|
+
const initObj:ClientOptions = {
|
|
22
|
+
dsn: settings.Sentry.dsn,
|
|
23
|
+
debug: isDebug,
|
|
24
|
+
integrations: [],
|
|
25
|
+
...settings?.Sentry?.initArgs,
|
|
26
|
+
}
|
|
27
|
+
if (settings?.Sentry?.beforeBreadcrumb) {
|
|
28
|
+
initObj.beforeBreadcrumb = settings?.Sentry?.beforeBreadcrumb
|
|
29
|
+
}
|
|
30
|
+
this.sentry?.init?.(initObj)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
captureBreadcrumb(type: LogType, content: LogFunctionArgs) {
|
|
35
|
+
if (!this.use) return
|
|
36
|
+
|
|
37
|
+
const [message, data, category] = content
|
|
38
|
+
|
|
39
|
+
const sentryArgs: Breadcrumb = {
|
|
40
|
+
message,
|
|
41
|
+
data,
|
|
42
|
+
category,
|
|
43
|
+
level: SentrySeverityMap[type],
|
|
44
|
+
type: '',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.sentry.addBreadcrumb(sentryArgs)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
sendLog(err?: any) {
|
|
51
|
+
if (!this.use) return
|
|
52
|
+
this.sentry.captureException(err)
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/Slack.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { inspect } from 'util'
|
|
2
|
+
import { TypeGuards, AppSettings } from '@codeleap/types'
|
|
3
|
+
|
|
4
|
+
type EchoSlackConfig = AppSettings['Slack']['echo']
|
|
5
|
+
|
|
6
|
+
type EchoSlack = {
|
|
7
|
+
label: string
|
|
8
|
+
data: object
|
|
9
|
+
options?: EchoSlackOptions
|
|
10
|
+
module?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type OptionInclude = 'version'
|
|
14
|
+
|
|
15
|
+
type SendIn = 'debug' | 'release'
|
|
16
|
+
|
|
17
|
+
type EchoSlackOptions = {
|
|
18
|
+
sendIn?: SendIn[]
|
|
19
|
+
include?: OptionInclude[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CHANNEL = '#_dev_logs'
|
|
23
|
+
const DEFAULT_BASE_URL = 'https://slack.com/api/chat.postMessage'
|
|
24
|
+
|
|
25
|
+
export class SlackService {
|
|
26
|
+
private echoConfig: EchoSlackConfig
|
|
27
|
+
|
|
28
|
+
private isDev: boolean
|
|
29
|
+
|
|
30
|
+
private appName: string
|
|
31
|
+
|
|
32
|
+
public api
|
|
33
|
+
|
|
34
|
+
constructor(settings: AppSettings) {
|
|
35
|
+
this.echoConfig = settings?.Slack?.echo as EchoSlackConfig
|
|
36
|
+
this.isDev = settings?.Environment?.IsDev
|
|
37
|
+
this.appName = settings?.AppName
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async echo(
|
|
41
|
+
label: EchoSlack['label'],
|
|
42
|
+
slackData: EchoSlack['data'],
|
|
43
|
+
moduleName: EchoSlack['module'] = null,
|
|
44
|
+
messageOptions: EchoSlack['options'] = {}
|
|
45
|
+
) {
|
|
46
|
+
const options = this.parseOptions(messageOptions)
|
|
47
|
+
const slack = this.parseData(label, slackData, options.info, moduleName)
|
|
48
|
+
|
|
49
|
+
const enabled = TypeGuards.isBoolean(this.echoConfig.enabled) ? this.echoConfig.enabled : true
|
|
50
|
+
|
|
51
|
+
if (!options.send || !this.api || !this.echoConfig || !enabled) return
|
|
52
|
+
|
|
53
|
+
const settingsData = this?.echoConfig?.options ?? {}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const data = {
|
|
57
|
+
'channel': this?.echoConfig?.channel ?? DEFAULT_CHANNEL,
|
|
58
|
+
text: slack,
|
|
59
|
+
'username': `${this.appName} Log`,
|
|
60
|
+
'icon_url': this?.echoConfig?.icon,
|
|
61
|
+
...settingsData,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
await this.api.post('', data, {
|
|
65
|
+
baseURL: this?.echoConfig?.baseURL ?? DEFAULT_BASE_URL,
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${this?.echoConfig?.token}`,
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error('Failed to echo', err, 'logger echoSlack')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private serializers: Record<OptionInclude, (IsDev: boolean) => string> = {
|
|
76
|
+
version: (IsDev: boolean) => {
|
|
77
|
+
return IsDev ? 'debug' : 'release'
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private parseOptions(options: EchoSlackOptions) {
|
|
82
|
+
const {
|
|
83
|
+
sendIn = [],
|
|
84
|
+
include = [],
|
|
85
|
+
} = options
|
|
86
|
+
|
|
87
|
+
const hasSendIn = sendIn.length >= 1
|
|
88
|
+
const isDebug = hasSendIn ? sendIn.includes('debug') : true
|
|
89
|
+
const isRelease = hasSendIn ? sendIn.includes('release') : true
|
|
90
|
+
|
|
91
|
+
if (!isDebug && this.isDev || !isRelease && !this.isDev) {
|
|
92
|
+
return {
|
|
93
|
+
info: '',
|
|
94
|
+
send: false,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let str = ''
|
|
99
|
+
const separator = ' - '
|
|
100
|
+
|
|
101
|
+
include.forEach(k => {
|
|
102
|
+
const data = this.serializers[k]?.(this.isDev)
|
|
103
|
+
str = `${str}${str.length > 0 ? separator : ''}[${data}]`
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
info: str,
|
|
108
|
+
send: true,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private parseData(label: string, data: object, info: string, module?: string) {
|
|
113
|
+
const obj = !info ? data : {
|
|
114
|
+
...data,
|
|
115
|
+
info,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const args = [`${!module ? '' : `(${module}) `}${label}: `, obj]
|
|
119
|
+
|
|
120
|
+
const slack = args.map(i => {
|
|
121
|
+
if (typeof i === 'object') {
|
|
122
|
+
try {
|
|
123
|
+
return inspect(i, {
|
|
124
|
+
depth: 5,
|
|
125
|
+
compact: false,
|
|
126
|
+
showHidden: true,
|
|
127
|
+
})
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return `${i} (Unserializable value)`
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return String(i)
|
|
134
|
+
}).join(' ')
|
|
135
|
+
|
|
136
|
+
return slack
|
|
137
|
+
}
|
|
138
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ConsoleColor } from './types'
|
|
2
|
+
import { LogType } from '@codeleap/types'
|
|
3
|
+
|
|
4
|
+
export const foregroundColors = {
|
|
5
|
+
Black: '\x1b[30m',
|
|
6
|
+
Red: '\x1b[31m',
|
|
7
|
+
Green: '\x1b[32m',
|
|
8
|
+
Yellow: '\x1b[33m',
|
|
9
|
+
Blue: '\x1b[34m',
|
|
10
|
+
Magenta: '\x1b[35m',
|
|
11
|
+
Cyan: '\x1b[36m',
|
|
12
|
+
White: '\x1b[37m',
|
|
13
|
+
} as const
|
|
14
|
+
|
|
15
|
+
export const formatColors = {
|
|
16
|
+
Reset: '\x1b[0m',
|
|
17
|
+
Bright: '\x1b[1m',
|
|
18
|
+
Dim: '\x1b[2m',
|
|
19
|
+
Underscore: '\x1b[4m',
|
|
20
|
+
Blink: '\x1b[5m',
|
|
21
|
+
Reverse: '\x1b[7m',
|
|
22
|
+
Hidden: '\x1b[8m',
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
export const colors = {
|
|
26
|
+
...foregroundColors,
|
|
27
|
+
...formatColors,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const logColors: Record<LogType, ConsoleColor> = {
|
|
31
|
+
error: 'Red',
|
|
32
|
+
info: 'White',
|
|
33
|
+
warn: 'Yellow',
|
|
34
|
+
debug: 'Magenta',
|
|
35
|
+
log: 'White',
|
|
36
|
+
silent: 'Green',
|
|
37
|
+
}
|
package/src/index.ts
ADDED
package/src/obfuscate.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { cloneDeep } from '@codeleap/utils'
|
|
2
|
+
import { Matcher } from '@codeleap/types'
|
|
3
|
+
import { inspect } from 'util'
|
|
4
|
+
import parse from 'url-parse'
|
|
5
|
+
|
|
6
|
+
type ObfuscateArgs = {
|
|
7
|
+
object: any
|
|
8
|
+
keys: (Matcher<'key'>)[]
|
|
9
|
+
values: (Matcher<'value'>)[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function removeKey(obj, key) {
|
|
13
|
+
if (obj?.hasOwnProperty(key)) {
|
|
14
|
+
obj[key] = '[secret]'
|
|
15
|
+
}
|
|
16
|
+
for (const subObj in obj) {
|
|
17
|
+
if (typeof obj[subObj] == 'object') {
|
|
18
|
+
removeKey(obj[subObj], key)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function removeValue(obj, value) {
|
|
24
|
+
for (const subObj in obj) {
|
|
25
|
+
const isString = typeof obj[subObj] == 'string'
|
|
26
|
+
if (isString) {
|
|
27
|
+
const isRegex = value instanceof RegExp
|
|
28
|
+
const match = isRegex ? value.test(obj[subObj]) : obj[subObj].includes(value)
|
|
29
|
+
if (match) {
|
|
30
|
+
if (obj[subObj].startsWith('http')) {
|
|
31
|
+
const url = parse(obj[subObj])
|
|
32
|
+
obj[subObj] = `${url.origin}${url.pathname}/[secret]`
|
|
33
|
+
} else {
|
|
34
|
+
obj[subObj] = '[secret]'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof obj[subObj] == 'object') {
|
|
39
|
+
removeValue(obj[subObj], value)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function obfuscate(args: ObfuscateArgs) {
|
|
45
|
+
const { object, keys, values } = args
|
|
46
|
+
|
|
47
|
+
let isCircular = false
|
|
48
|
+
try {
|
|
49
|
+
JSON.stringify(args)
|
|
50
|
+
} catch (e) {
|
|
51
|
+
isCircular = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (typeof object === 'object' && !isCircular) {
|
|
55
|
+
let cleanData = {}
|
|
56
|
+
try {
|
|
57
|
+
cleanData = cloneDeep(object)
|
|
58
|
+
keys.forEach(fieldName => removeKey(cleanData, fieldName))
|
|
59
|
+
values.forEach(fieldName => removeValue(cleanData, fieldName))
|
|
60
|
+
} catch (err1) {
|
|
61
|
+
try {
|
|
62
|
+
cleanData = inspect(object, { depth: 1 })
|
|
63
|
+
} catch (err2) {
|
|
64
|
+
cleanData = { value: `Couldn't process data` }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const result = cleanData
|
|
68
|
+
return result
|
|
69
|
+
} else {
|
|
70
|
+
if (isCircular) {
|
|
71
|
+
return { ...args.object, WARNING: 'Circular reference detected' }
|
|
72
|
+
} else {
|
|
73
|
+
return args.object
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { InspectRenderOptions } from '.'
|
|
2
|
+
|
|
3
|
+
type ErrorArgs = InspectRenderOptions & {
|
|
4
|
+
name: string
|
|
5
|
+
maxRenders: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type ErrorNames = 'maxRenders'
|
|
9
|
+
|
|
10
|
+
const defineError = (errorName: ErrorNames, args: Partial<ErrorArgs>) => {
|
|
11
|
+
switch (errorName) {
|
|
12
|
+
case 'maxRenders':
|
|
13
|
+
return `${args.name} is rendering more than ${args.maxRenders}time per ${
|
|
14
|
+
args.throttleInterval / 1000
|
|
15
|
+
}second!
|
|
16
|
+
If you aware of this, you can disable it in the Settings.ts > Performancer.
|
|
17
|
+
`
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class PerformanceError extends Error {
|
|
22
|
+
constructor(errorName: ErrorNames, args: Partial<ErrorArgs>) {
|
|
23
|
+
super(defineError(errorName, args))
|
|
24
|
+
this.name = 'Codeleap:Perf'
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { throttle } from '@codeleap/utils'
|
|
2
|
+
import { useEffect } from 'react'
|
|
3
|
+
import { AppSettings } from '@codeleap/types'
|
|
4
|
+
import { PerformanceError } from './errors'
|
|
5
|
+
|
|
6
|
+
export type InspectRenderOptions = {
|
|
7
|
+
noHooks?: boolean
|
|
8
|
+
logMode?: 'raw' | 'summarized'
|
|
9
|
+
throttleInterval?: number
|
|
10
|
+
maxRenders?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PerformanceInspector = {
|
|
14
|
+
inspectRender: (name: string, options?: InspectRenderOptions) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const renderCounter: Record<string, number> = {}
|
|
18
|
+
|
|
19
|
+
export function makePerformanceInspector(settings: AppSettings) {
|
|
20
|
+
/**
|
|
21
|
+
* inspectRender monitors how much time a component render per second.
|
|
22
|
+
* Use perf.inspectRender('ComponentName') inside a component to monitor it.
|
|
23
|
+
* @param {string} name - Component name
|
|
24
|
+
* @param {PerformanceInspector} options - Some options for the inspector
|
|
25
|
+
* @returns
|
|
26
|
+
*/
|
|
27
|
+
const inspectRender = (
|
|
28
|
+
name: string,
|
|
29
|
+
options: InspectRenderOptions = {
|
|
30
|
+
noHooks: false,
|
|
31
|
+
logMode: 'summarized',
|
|
32
|
+
throttleInterval: 1000,
|
|
33
|
+
maxRenders: settings?.PerformanceInspector.maxRenders,
|
|
34
|
+
},
|
|
35
|
+
) => {
|
|
36
|
+
const blacklist = settings?.PerformanceInspector.blacklist || []
|
|
37
|
+
if (blacklist.some((item) => name.startsWith(item))) return
|
|
38
|
+
|
|
39
|
+
const { noHooks, logMode, throttleInterval, maxRenders } = options
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
!settings?.PerformanceInspector.enable ||
|
|
43
|
+
!settings?.Environment.IsDev
|
|
44
|
+
) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!noHooks) {
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
console.log(`[PerformanceInspector] Mounted -> ${name}`)
|
|
51
|
+
|
|
52
|
+
return () => {
|
|
53
|
+
console.log(`[PerformanceInspector] Unmounted -> ${name}`)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
renderCounter[name] = renderCounter[name] ? renderCounter[name] + 1 : 1
|
|
59
|
+
const renders = renderCounter[name]
|
|
60
|
+
|
|
61
|
+
if (renders > maxRenders) {
|
|
62
|
+
renderCounter[name] = 0
|
|
63
|
+
throw new PerformanceError('maxRenders', {
|
|
64
|
+
name,
|
|
65
|
+
throttleInterval,
|
|
66
|
+
maxRenders,
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (logMode === 'raw') {
|
|
71
|
+
console.log(`[PerformanceInspector] Rendered -> ${name}: ${renders}`)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function logSummary() {
|
|
76
|
+
if (renders <= 0) return
|
|
77
|
+
|
|
78
|
+
console.log(`[PerformanceInspector] Render summary -> ${name}: ${renders}`)
|
|
79
|
+
renderCounter[name] = 0
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
throttle(logSummary, name, throttleInterval)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { inspectRender }
|
|
86
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FunctionType } from '@codeleap/types'
|
|
2
|
+
import { colors, foregroundColors } from './constants'
|
|
3
|
+
|
|
4
|
+
import type { SeverityLevel, Client, ClientOptions, Breadcrumb } from '@sentry/types'
|
|
5
|
+
|
|
6
|
+
export type LogType = 'info' | 'debug' | 'warn' | 'error' | 'log' | 'silent'
|
|
7
|
+
|
|
8
|
+
export type LogFunctionArgs = [
|
|
9
|
+
description?: any,
|
|
10
|
+
value?:any,
|
|
11
|
+
category?:string
|
|
12
|
+
]
|
|
13
|
+
export type ConsoleColor = keyof typeof colors
|
|
14
|
+
export type DebugColors = {
|
|
15
|
+
[Property in keyof typeof foregroundColors as `${Lowercase<
|
|
16
|
+
string & Property
|
|
17
|
+
>}`]: (...args: LogFunctionArgs) => void;
|
|
18
|
+
}
|
|
19
|
+
export type DebugColor = keyof DebugColors
|
|
20
|
+
export type LogToTerminalArgs = {
|
|
21
|
+
logType: LogType
|
|
22
|
+
args: LogFunctionArgs
|
|
23
|
+
color?: keyof DebugColors
|
|
24
|
+
deviceIdentifier?: string
|
|
25
|
+
stringify?: boolean
|
|
26
|
+
logKeys?: boolean
|
|
27
|
+
}
|
|
28
|
+
export type LogToTerminal = FunctionType<[LogToTerminalArgs], void>
|
|
29
|
+
|
|
30
|
+
export const SentrySeverityMap: Record<LogType, SeverityLevel> = {
|
|
31
|
+
debug: 'debug',
|
|
32
|
+
error: 'error',
|
|
33
|
+
info: 'info',
|
|
34
|
+
log: 'log',
|
|
35
|
+
warn: 'warning',
|
|
36
|
+
silent: 'log',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type SentryProvider = {
|
|
40
|
+
addBreadcrumb: FunctionType<[Breadcrumb], void>
|
|
41
|
+
init(options: ClientOptions): Client
|
|
42
|
+
captureException(err: any): void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type LoggerMiddleware = FunctionType<[arguments: LogToTerminalArgs, formattedContent: string[]], any>
|