dd-trace 5.47.0 → 5.48.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/index.d.ts +39 -0
- package/package.json +1 -1
- package/packages/dd-trace/src/appsec/sdk/index.js +23 -1
- package/packages/dd-trace/src/appsec/sdk/noop.js +10 -0
- package/packages/dd-trace/src/appsec/sdk/set_user.js +2 -1
- package/packages/dd-trace/src/appsec/sdk/track_event.js +129 -8
- package/packages/dd-trace/src/appsec/sdk/user_blocking.js +0 -1
- package/packages/dd-trace/src/appsec/telemetry/index.js +2 -2
- package/packages/dd-trace/src/appsec/telemetry/user.js +2 -2
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +36 -2
- package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -33
- package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -1
- package/packages/dd-trace/src/debugger/devtools_client/source-maps.js +1 -1
package/index.d.ts
CHANGED
|
@@ -869,6 +869,39 @@ declare namespace tracer {
|
|
|
869
869
|
flush(): void
|
|
870
870
|
}
|
|
871
871
|
|
|
872
|
+
export interface EventTrackingV2 {
|
|
873
|
+
/**
|
|
874
|
+
* Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally.
|
|
875
|
+
* @param {string} login The login key (username, email...) used by the user to authenticate.
|
|
876
|
+
* @param {User} user Properties of the authenticated user. Accepts custom fields. Can be null.
|
|
877
|
+
* @param {any} metadata Custom fields to link to the login success event.
|
|
878
|
+
*/
|
|
879
|
+
trackUserLoginSuccess(login: string, user?: User | null, metadata?: any): void;
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally.
|
|
883
|
+
* @param {string} login The login key (username, email...) used by the user to authenticate.
|
|
884
|
+
* @param {string} userId Identifier of the authenticated user.
|
|
885
|
+
* @param {any} metadata Custom fields to link to the login success event.
|
|
886
|
+
*/
|
|
887
|
+
trackUserLoginSuccess(login: string, userId: string, metadata?: any): void;
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Links a failed login event to the current trace.
|
|
891
|
+
* @param {string} login The login key (username, email...) used by the user to authenticate.
|
|
892
|
+
* @param {boolean} exists If the user exists.
|
|
893
|
+
* @param {any} metadata Custom fields to link to the login failure event.
|
|
894
|
+
*/
|
|
895
|
+
trackUserLoginFailure(login: string, exists: boolean, metadata?: any): void;
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Links a failed login event to the current trace.
|
|
899
|
+
* @param {string} login The login key (username, email...) used by the user to authenticate.
|
|
900
|
+
* @param {any} metadata Custom fields to link to the login failure event.
|
|
901
|
+
*/
|
|
902
|
+
trackUserLoginFailure(login: string, metadata?: any): void;
|
|
903
|
+
}
|
|
904
|
+
|
|
872
905
|
export interface Appsec {
|
|
873
906
|
/**
|
|
874
907
|
* Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally.
|
|
@@ -876,6 +909,8 @@ declare namespace tracer {
|
|
|
876
909
|
* @param {[key: string]: string} metadata Custom fields to link to the login success event.
|
|
877
910
|
*
|
|
878
911
|
* @beta This method is in beta and could change in future versions.
|
|
912
|
+
*
|
|
913
|
+
* @deprecated In favor of eventTrackingV2.trackUserLoginSuccess
|
|
879
914
|
*/
|
|
880
915
|
trackUserLoginSuccessEvent(user: User, metadata?: { [key: string]: string }): void
|
|
881
916
|
|
|
@@ -886,6 +921,8 @@ declare namespace tracer {
|
|
|
886
921
|
* @param {[key: string]: string} metadata Custom fields to link to the login failure event.
|
|
887
922
|
*
|
|
888
923
|
* @beta This method is in beta and could change in future versions.
|
|
924
|
+
*
|
|
925
|
+
* @deprecated In favor of eventTrackingV2.trackUserLoginFailure
|
|
889
926
|
*/
|
|
890
927
|
trackUserLoginFailureEvent(userId: string, exists: boolean, metadata?: { [key: string]: string }): void
|
|
891
928
|
|
|
@@ -926,6 +963,8 @@ declare namespace tracer {
|
|
|
926
963
|
* @beta This method is in beta and could change in the future
|
|
927
964
|
*/
|
|
928
965
|
setUser(user: User): void
|
|
966
|
+
|
|
967
|
+
eventTrackingV2: EventTrackingV2
|
|
929
968
|
}
|
|
930
969
|
|
|
931
970
|
/** @hidden */
|
package/package.json
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
trackUserLoginSuccessEvent,
|
|
5
|
+
trackUserLoginFailureEvent,
|
|
6
|
+
trackCustomEvent,
|
|
7
|
+
trackUserLoginSuccessV2,
|
|
8
|
+
trackUserLoginFailureV2
|
|
9
|
+
} = require('./track_event')
|
|
4
10
|
const { checkUserAndSetUser, blockRequest } = require('./user_blocking')
|
|
5
11
|
const { setTemplates } = require('../blocking')
|
|
6
12
|
const { setUser } = require('./set_user')
|
|
7
13
|
|
|
14
|
+
class EventTrackingV2 {
|
|
15
|
+
constructor (tracer) {
|
|
16
|
+
this._tracer = tracer
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
trackUserLoginSuccess (login, user, metadata) {
|
|
20
|
+
trackUserLoginSuccessV2(this._tracer, login, user, metadata)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
trackUserLoginFailure (login, exists, metadata) {
|
|
24
|
+
trackUserLoginFailureV2(this._tracer, login, exists, metadata)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
8
28
|
class AppsecSdk {
|
|
9
29
|
constructor (tracer, config) {
|
|
10
30
|
this._tracer = tracer
|
|
11
31
|
if (config) {
|
|
12
32
|
setTemplates(config)
|
|
13
33
|
}
|
|
34
|
+
|
|
35
|
+
this.eventTrackingV2 = new EventTrackingV2(tracer)
|
|
14
36
|
}
|
|
15
37
|
|
|
16
38
|
trackUserLoginSuccessEvent (user, metadata) {
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
class NoopEventTrackingV2 {
|
|
4
|
+
trackUserLoginSuccess () {}
|
|
5
|
+
|
|
6
|
+
trackUserLoginFailure () {}
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
class NoopAppsecSdk {
|
|
10
|
+
constructor () {
|
|
11
|
+
this.eventTrackingV2 = new NoopEventTrackingV2()
|
|
12
|
+
}
|
|
13
|
+
|
|
4
14
|
trackUserLoginSuccessEvent () {}
|
|
5
15
|
|
|
6
16
|
trackUserLoginFailureEvent () {}
|
|
@@ -9,6 +9,8 @@ function setUserTags (user, rootSpan) {
|
|
|
9
9
|
for (const k of Object.keys(user)) {
|
|
10
10
|
rootSpan.setTag(`usr.${k}`, '' + user[k])
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
rootSpan.setTag('_dd.appsec.user.collection_mode', 'sdk')
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
function setUser (tracer, user) {
|
|
@@ -24,7 +26,6 @@ function setUser (tracer, user) {
|
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
setUserTags(user, rootSpan)
|
|
27
|
-
rootSpan.setTag('_dd.appsec.user.collection_mode', 'sdk')
|
|
28
29
|
|
|
29
30
|
const persistent = {
|
|
30
31
|
[addresses.USER_ID]: '' + user.id
|
|
@@ -9,6 +9,9 @@ const addresses = require('../addresses')
|
|
|
9
9
|
const { ASM } = require('../../standalone/product')
|
|
10
10
|
const { incrementSdkEventMetric } = require('../telemetry')
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated in favor of trackUserLoginSuccessV2
|
|
14
|
+
*/
|
|
12
15
|
function trackUserLoginSuccessEvent (tracer, user, metadata) {
|
|
13
16
|
// TODO: better user check here and in _setUser() ?
|
|
14
17
|
if (!user || !user.id) {
|
|
@@ -16,7 +19,7 @@ function trackUserLoginSuccessEvent (tracer, user, metadata) {
|
|
|
16
19
|
return
|
|
17
20
|
}
|
|
18
21
|
|
|
19
|
-
incrementSdkEventMetric('login_success')
|
|
22
|
+
incrementSdkEventMetric('login_success', 'v1')
|
|
20
23
|
|
|
21
24
|
const rootSpan = getRootSpan(tracer)
|
|
22
25
|
if (!rootSpan) {
|
|
@@ -35,6 +38,9 @@ function trackUserLoginSuccessEvent (tracer, user, metadata) {
|
|
|
35
38
|
runWaf('users.login.success', { id: user.id, login })
|
|
36
39
|
}
|
|
37
40
|
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated in favor of trackUserLoginFailureV2
|
|
43
|
+
*/
|
|
38
44
|
function trackUserLoginFailureEvent (tracer, userId, exists, metadata) {
|
|
39
45
|
if (!userId || typeof userId !== 'string') {
|
|
40
46
|
log.warn('[ASM] Invalid userId provided to trackUserLoginFailureEvent')
|
|
@@ -52,7 +58,7 @@ function trackUserLoginFailureEvent (tracer, userId, exists, metadata) {
|
|
|
52
58
|
|
|
53
59
|
runWaf('users.login.failure', { login: userId })
|
|
54
60
|
|
|
55
|
-
incrementSdkEventMetric('login_failure')
|
|
61
|
+
incrementSdkEventMetric('login_failure', 'v1')
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
function trackCustomEvent (tracer, eventName, metadata) {
|
|
@@ -63,7 +69,112 @@ function trackCustomEvent (tracer, eventName, metadata) {
|
|
|
63
69
|
|
|
64
70
|
trackEvent(eventName, metadata, 'trackCustomEvent', getRootSpan(tracer))
|
|
65
71
|
|
|
66
|
-
incrementSdkEventMetric('custom')
|
|
72
|
+
incrementSdkEventMetric('custom', 'v1')
|
|
73
|
+
|
|
74
|
+
if (eventName === 'users.login.success' || eventName === 'users.login.failure') {
|
|
75
|
+
runWaf(eventName)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function trackUserLoginSuccessV2 (tracer, login, user, metadata) {
|
|
80
|
+
if (!login || typeof login !== 'string') {
|
|
81
|
+
log.warn('[ASM] Invalid login provided to eventTrackingV2.trackUserLoginSuccess')
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
incrementSdkEventMetric('login_success', 'v2')
|
|
86
|
+
|
|
87
|
+
const rootSpan = getRootSpan(tracer)
|
|
88
|
+
if (!rootSpan) {
|
|
89
|
+
log.warn('[ASM] Root span not available in eventTrackingV2.trackUserLoginSuccess')
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const wafData = { login }
|
|
94
|
+
|
|
95
|
+
metadata = {
|
|
96
|
+
'usr.login': login,
|
|
97
|
+
...metadata
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (user) {
|
|
101
|
+
if (typeof user !== 'object') {
|
|
102
|
+
user = { id: user }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (user.id) {
|
|
106
|
+
wafData.id = user.id
|
|
107
|
+
setUserTags(user, rootSpan)
|
|
108
|
+
metadata.usr = user
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
trackEvent('users.login.success', metadata, 'eventTrackingV2.trackUserLoginSuccess', rootSpan)
|
|
113
|
+
|
|
114
|
+
runWaf('users.login.success', wafData)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function trackUserLoginFailureV2 (tracer, login, exists, metadata) {
|
|
118
|
+
if (!login || typeof login !== 'string') {
|
|
119
|
+
log.warn('[ASM] Invalid login provided to eventTrackingV2.trackUserLoginFailure')
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
incrementSdkEventMetric('login_failure', 'v2')
|
|
124
|
+
|
|
125
|
+
const rootSpan = getRootSpan(tracer)
|
|
126
|
+
if (!rootSpan) {
|
|
127
|
+
log.warn('[ASM] Root span not available in eventTrackingV2.trackUserLoginFailure')
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const wafData = { login }
|
|
132
|
+
|
|
133
|
+
if (typeof exists === 'object' && metadata === undefined) {
|
|
134
|
+
metadata = exists
|
|
135
|
+
exists = false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
metadata = {
|
|
139
|
+
'usr.login': login,
|
|
140
|
+
'usr.exists': exists ? 'true' : 'false',
|
|
141
|
+
...metadata
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
trackEvent('users.login.failure', metadata, 'eventTrackingV2.trackUserLoginFailure', rootSpan)
|
|
145
|
+
|
|
146
|
+
runWaf('users.login.failure', wafData)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function flattenFields (fields, depth = 0) {
|
|
150
|
+
if (depth > 4) {
|
|
151
|
+
return {
|
|
152
|
+
truncated: true
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const result = {}
|
|
157
|
+
let truncated = false
|
|
158
|
+
for (const key of Object.keys(fields)) {
|
|
159
|
+
const value = fields[key]
|
|
160
|
+
|
|
161
|
+
if (value && typeof value === 'object') {
|
|
162
|
+
const { result: flatValue, truncated: inheritTruncated } = flattenFields(value, depth + 1)
|
|
163
|
+
truncated = truncated || inheritTruncated
|
|
164
|
+
|
|
165
|
+
if (flatValue) {
|
|
166
|
+
for (const flatKey of Object.keys(flatValue)) {
|
|
167
|
+
result[`${key}.${flatKey}`] = flatValue[flatKey]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
if (value !== undefined) {
|
|
172
|
+
result[key] = value
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return { result, truncated }
|
|
67
178
|
}
|
|
68
179
|
|
|
69
180
|
function trackEvent (eventName, fields, sdkMethodName, rootSpan) {
|
|
@@ -78,8 +189,14 @@ function trackEvent (eventName, fields, sdkMethodName, rootSpan) {
|
|
|
78
189
|
}
|
|
79
190
|
|
|
80
191
|
if (fields) {
|
|
81
|
-
|
|
82
|
-
|
|
192
|
+
const { result: flatFields, truncated } = flattenFields(fields)
|
|
193
|
+
|
|
194
|
+
if (truncated) {
|
|
195
|
+
log.warn('[ASM] Too deep object provided in the SDK method %s, object truncated', sdkMethodName)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for (const metadataKey of Object.keys(flatFields)) {
|
|
199
|
+
tags[`appsec.events.${eventName}.${metadataKey}`] = '' + flatFields[metadataKey]
|
|
83
200
|
}
|
|
84
201
|
}
|
|
85
202
|
|
|
@@ -93,11 +210,11 @@ function runWaf (eventName, user) {
|
|
|
93
210
|
[`server.business_logic.${eventName}`]: null
|
|
94
211
|
}
|
|
95
212
|
|
|
96
|
-
if (user
|
|
213
|
+
if (user?.id) {
|
|
97
214
|
persistent[addresses.USER_ID] = '' + user.id
|
|
98
215
|
}
|
|
99
216
|
|
|
100
|
-
if (user
|
|
217
|
+
if (user?.login) {
|
|
101
218
|
persistent[addresses.USER_LOGIN] = '' + user.login
|
|
102
219
|
}
|
|
103
220
|
|
|
@@ -107,5 +224,9 @@ function runWaf (eventName, user) {
|
|
|
107
224
|
module.exports = {
|
|
108
225
|
trackUserLoginSuccessEvent,
|
|
109
226
|
trackUserLoginFailureEvent,
|
|
110
|
-
trackCustomEvent
|
|
227
|
+
trackCustomEvent,
|
|
228
|
+
trackUserLoginSuccessV2,
|
|
229
|
+
trackUserLoginFailureV2,
|
|
230
|
+
trackEvent,
|
|
231
|
+
runWaf
|
|
111
232
|
}
|
|
@@ -23,7 +23,6 @@ function checkUserAndSetUser (tracer, user) {
|
|
|
23
23
|
if (rootSpan) {
|
|
24
24
|
if (!rootSpan.context()._tags['usr.id']) {
|
|
25
25
|
setUserTags(user, rootSpan)
|
|
26
|
-
rootSpan.setTag('_dd.appsec.user.collection_mode', 'sdk')
|
|
27
26
|
}
|
|
28
27
|
} else {
|
|
29
28
|
log.warn('[ASM] Root span not available in isUserBlocked')
|
|
@@ -143,10 +143,10 @@ function incrementMissingUserIdMetric (framework, eventType) {
|
|
|
143
143
|
incrementMissingUserId(framework, eventType)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
function incrementSdkEventMetric (
|
|
146
|
+
function incrementSdkEventMetric (eventType, sdkVersion) {
|
|
147
147
|
if (!enabled) return
|
|
148
148
|
|
|
149
|
-
incrementSdkEvent(
|
|
149
|
+
incrementSdkEvent(eventType, sdkVersion)
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function getRequestMetrics (req) {
|
|
@@ -18,10 +18,10 @@ function incrementMissingUserId (framework, eventType) {
|
|
|
18
18
|
}).inc()
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function incrementSdkEvent (eventType) {
|
|
21
|
+
function incrementSdkEvent (eventType, sdkVersion = 'v1') {
|
|
22
22
|
appsecMetrics.count('sdk.event', {
|
|
23
23
|
event_type: eventType,
|
|
24
|
-
sdk_version:
|
|
24
|
+
sdk_version: sdkVersion
|
|
25
25
|
}).inc()
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { getGeneratedPosition } = require('./source-maps')
|
|
4
4
|
const lock = require('./lock')()
|
|
5
5
|
const session = require('./session')
|
|
6
|
-
const compileCondition = require('./condition')
|
|
6
|
+
const { compile: compileCondition, compileSegments, templateRequiresEvaluation } = require('./condition')
|
|
7
7
|
const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
|
|
8
8
|
const { findScriptFromPartialPath, locationToBreakpoint, breakpointToProbes, probeToLocation } = require('./state')
|
|
9
9
|
const log = require('../../log')
|
|
@@ -26,6 +26,13 @@ async function addBreakpoint (probe) {
|
|
|
26
26
|
probe.location = { file, lines: [String(lineNumber)] }
|
|
27
27
|
delete probe.where
|
|
28
28
|
|
|
29
|
+
// Optimize for fast calculations when probe is hit
|
|
30
|
+
probe.templateRequiresEvaluation = templateRequiresEvaluation(probe.segments)
|
|
31
|
+
if (probe.templateRequiresEvaluation) {
|
|
32
|
+
probe.template = compileSegments(probe.segments)
|
|
33
|
+
}
|
|
34
|
+
delete probe.segments
|
|
35
|
+
|
|
29
36
|
// Optimize for fast calculations when probe is hit
|
|
30
37
|
const snapshotsPerSecond = probe.sampling?.snapshotsPerSecond ?? (probe.captureSnapshot
|
|
31
38
|
? MAX_SNAPSHOTS_PER_SECOND_PER_PROBE
|
|
@@ -41,6 +48,10 @@ async function addBreakpoint (probe) {
|
|
|
41
48
|
const { url, scriptId, sourceMapURL, source } = script
|
|
42
49
|
|
|
43
50
|
if (sourceMapURL) {
|
|
51
|
+
log.debug(
|
|
52
|
+
'[debugger:devtools_client] Translating location using source map for %s:%d:%d (probe: %s, version: %d)',
|
|
53
|
+
file, lineNumber, columnNumber, probe.id, probe.version
|
|
54
|
+
);
|
|
44
55
|
({ line: lineNumber, column: columnNumber } = await getGeneratedPosition(url, source, lineNumber, sourceMapURL))
|
|
45
56
|
}
|
|
46
57
|
|
|
@@ -156,6 +167,8 @@ function stop () {
|
|
|
156
167
|
|
|
157
168
|
// Only if all probes have a condition can we use a compound condition.
|
|
158
169
|
// Otherwise, we need to evaluate each probe individually once the breakpoint is hit.
|
|
170
|
+
// TODO: Handle errors - if there's 2 conditons, and one fails but the other returns true, we should still pause the
|
|
171
|
+
// breakpoint
|
|
159
172
|
function compileCompoundCondition (probes) {
|
|
160
173
|
return probes.every(p => p.condition)
|
|
161
174
|
? probes.map(p => p.condition).filter(Boolean).join(' || ')
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
module.exports =
|
|
3
|
+
module.exports = {
|
|
4
|
+
compile,
|
|
5
|
+
compileSegments,
|
|
6
|
+
templateRequiresEvaluation
|
|
7
|
+
}
|
|
4
8
|
|
|
5
9
|
const identifierRegex = /^[@a-zA-Z_$][\w$]*$/
|
|
6
10
|
|
|
@@ -35,7 +39,37 @@ const reservedWords = new Set([
|
|
|
35
39
|
|
|
36
40
|
const PRIMITIVE_TYPES = new Set(['string', 'number', 'bigint', 'boolean', 'undefined', 'symbol', 'null'])
|
|
37
41
|
|
|
38
|
-
|
|
42
|
+
function templateRequiresEvaluation (segments) {
|
|
43
|
+
if (segments === undefined) return false // There should always be segments, but just in case
|
|
44
|
+
for (const { str } of segments) {
|
|
45
|
+
if (str === undefined) return true
|
|
46
|
+
}
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compileSegments (segments) {
|
|
51
|
+
let result = '['
|
|
52
|
+
for (let i = 0; i < segments.length; i++) {
|
|
53
|
+
const { str, dsl, json } = segments[i]
|
|
54
|
+
result += str !== undefined
|
|
55
|
+
? JSON.stringify(str)
|
|
56
|
+
: `(() => {
|
|
57
|
+
try {
|
|
58
|
+
const result = ${compile(json)}
|
|
59
|
+
return typeof result === 'string' ? result : $dd_inspect(result, $dd_segmentInspectOptions)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return { expr: ${JSON.stringify(dsl)}, message: \`\${e.name}: \${e.message}\` }
|
|
62
|
+
}
|
|
63
|
+
})()`
|
|
64
|
+
if (i !== segments.length - 1) {
|
|
65
|
+
result += ','
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return `${result}]`
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// TODO: Consider storing some of these functions that doesn't require closure access to the current scope on `process`
|
|
72
|
+
// so they can be reused across probes
|
|
39
73
|
function compile (node) {
|
|
40
74
|
if (node === null || typeof node === 'number' || typeof node === 'boolean') {
|
|
41
75
|
return node
|
|
@@ -16,10 +16,20 @@ const { NODE_MAJOR } = require('../../../../../version')
|
|
|
16
16
|
require('./remote_config')
|
|
17
17
|
|
|
18
18
|
// Expression to run on a call frame of the paused thread to get its active trace and span id.
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
19
|
+
const templateExpressionSetupCode = `
|
|
20
|
+
const $dd_inspect = global.require('node:util').inspect;
|
|
21
|
+
const $dd_segmentInspectOptions = {
|
|
22
|
+
depth: 0,
|
|
23
|
+
customInspect: false,
|
|
24
|
+
maxArrayLength: 3,
|
|
25
|
+
maxStringLength: 8 * 1024,
|
|
26
|
+
breakLength: Infinity
|
|
27
|
+
};
|
|
22
28
|
`
|
|
29
|
+
const getDDTagsExpression = `(() => {
|
|
30
|
+
const context = global.require('dd-trace').scope().active()?.context();
|
|
31
|
+
return { trace_id: context?.toTraceId(), span_id: context?.toSpanId() }
|
|
32
|
+
})()`
|
|
23
33
|
|
|
24
34
|
// There doesn't seem to be an official standard for the content of these fields, so we're just populating them with
|
|
25
35
|
// something that should be useful to a Node.js developer.
|
|
@@ -45,6 +55,7 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
45
55
|
let sampled = false
|
|
46
56
|
let numberOfProbesWithSnapshots = 0
|
|
47
57
|
const probes = []
|
|
58
|
+
let templateExpressions = ''
|
|
48
59
|
|
|
49
60
|
// V8 doesn't allow setting more than one breakpoint at a specific location, however, it's possible to set two
|
|
50
61
|
// breakpoints just next to each other that will "snap" to the same logical location, which in turn will be hit at the
|
|
@@ -79,15 +90,6 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
79
90
|
continue
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
if (shouldVerifyConditions && probe.condition !== undefined) {
|
|
83
|
-
const { result } = await session.post('Debugger.evaluateOnCallFrame', {
|
|
84
|
-
callFrameId: params.callFrames[0].callFrameId,
|
|
85
|
-
expression: probe.condition,
|
|
86
|
-
returnByValue: true
|
|
87
|
-
})
|
|
88
|
-
if (result.value !== true) continue
|
|
89
|
-
}
|
|
90
|
-
|
|
91
93
|
if (probe.captureSnapshot === true) {
|
|
92
94
|
// This algorithm to calculate number of sampled snapshots within the last second is not perfect, as it's not a
|
|
93
95
|
// sliding window. But it's quick and easy :)
|
|
@@ -107,9 +109,24 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
107
109
|
maxLength = highestOrUndefined(probe.capture.maxLength, maxLength)
|
|
108
110
|
}
|
|
109
111
|
|
|
112
|
+
if (shouldVerifyConditions && probe.condition !== undefined) {
|
|
113
|
+
// TODO: Bundle all conditions and evaluate them in a single call
|
|
114
|
+
// TODO: Handle errors
|
|
115
|
+
const { result } = await session.post('Debugger.evaluateOnCallFrame', {
|
|
116
|
+
callFrameId: params.callFrames[0].callFrameId,
|
|
117
|
+
expression: probe.condition,
|
|
118
|
+
returnByValue: true
|
|
119
|
+
})
|
|
120
|
+
if (result.value !== true) continue
|
|
121
|
+
}
|
|
122
|
+
|
|
110
123
|
sampled = true
|
|
111
124
|
probe.lastCaptureNs = start
|
|
112
125
|
|
|
126
|
+
if (probe.templateRequiresEvaluation) {
|
|
127
|
+
templateExpressions += `,${probe.template}`
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
probes.push(probe)
|
|
114
131
|
}
|
|
115
132
|
}
|
|
@@ -119,7 +136,22 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
const timestamp = Date.now()
|
|
122
|
-
|
|
139
|
+
|
|
140
|
+
let evalResults = null
|
|
141
|
+
const { result } = await session.post('Debugger.evaluateOnCallFrame', {
|
|
142
|
+
callFrameId: params.callFrames[0].callFrameId,
|
|
143
|
+
expression: templateExpressions.length === 0
|
|
144
|
+
? `[${getDDTagsExpression}]`
|
|
145
|
+
: `${templateExpressionSetupCode}[${getDDTagsExpression}${templateExpressions}]`,
|
|
146
|
+
returnByValue: true,
|
|
147
|
+
includeCommandLineAPI: true
|
|
148
|
+
})
|
|
149
|
+
if (result?.subtype === 'error') {
|
|
150
|
+
log.error('[debugger:devtools_client] Error evaluating code on call frame: %s', result?.description)
|
|
151
|
+
evalResults = []
|
|
152
|
+
} else {
|
|
153
|
+
evalResults = result?.value ?? []
|
|
154
|
+
}
|
|
123
155
|
|
|
124
156
|
let processLocalState
|
|
125
157
|
if (numberOfProbesWithSnapshots !== 0) {
|
|
@@ -155,6 +187,8 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
155
187
|
}
|
|
156
188
|
|
|
157
189
|
const stack = getStackFromCallFrames(params.callFrames)
|
|
190
|
+
const dd = processDD(evalResults[0]) // the first result is the dd tags, the rest are the probe template results
|
|
191
|
+
let messageIndex = 1
|
|
158
192
|
|
|
159
193
|
// TODO: Send multiple probes in one HTTP request as an array (DEBUG-2848)
|
|
160
194
|
for (const probe of probes) {
|
|
@@ -179,9 +213,33 @@ session.on('Debugger.paused', async ({ params }) => {
|
|
|
179
213
|
}
|
|
180
214
|
}
|
|
181
215
|
|
|
216
|
+
let message = ''
|
|
217
|
+
if (probe.templateRequiresEvaluation) {
|
|
218
|
+
const results = evalResults[messageIndex++]
|
|
219
|
+
if (results === undefined) {
|
|
220
|
+
log.error('[debugger:devtools_client] No evaluation results for probe %s', probe.id)
|
|
221
|
+
} else {
|
|
222
|
+
for (const result of results) {
|
|
223
|
+
if (typeof result === 'string') {
|
|
224
|
+
message += result
|
|
225
|
+
} else {
|
|
226
|
+
// If `result` isn't a string, it's an evaluation error object
|
|
227
|
+
if (snapshot.evaluationErrors === undefined) {
|
|
228
|
+
snapshot.evaluationErrors = [result]
|
|
229
|
+
} else {
|
|
230
|
+
snapshot.evaluationErrors.push(result)
|
|
231
|
+
}
|
|
232
|
+
message += `{${result.message}}`
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} else {
|
|
237
|
+
message = probe.template
|
|
238
|
+
}
|
|
239
|
+
|
|
182
240
|
ackEmitting(probe)
|
|
183
|
-
|
|
184
|
-
send(
|
|
241
|
+
|
|
242
|
+
send(message, logger, dd, snapshot)
|
|
185
243
|
}
|
|
186
244
|
})
|
|
187
245
|
|
|
@@ -189,22 +247,6 @@ function highestOrUndefined (num, max) {
|
|
|
189
247
|
return num === undefined ? max : Math.max(num, max ?? 0)
|
|
190
248
|
}
|
|
191
249
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// `Runtime.releaseObjectGroup`
|
|
195
|
-
const { result } = await session.post('Debugger.evaluateOnCallFrame', {
|
|
196
|
-
callFrameId,
|
|
197
|
-
expression,
|
|
198
|
-
returnByValue: true,
|
|
199
|
-
includeCommandLineAPI: true
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
if (result?.value?.trace_id === undefined) {
|
|
203
|
-
if (result?.subtype === 'error') {
|
|
204
|
-
log.error('[debugger:devtools_client] Error getting trace/span id:', result.description)
|
|
205
|
-
}
|
|
206
|
-
return
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return result.value
|
|
250
|
+
function processDD (result) {
|
|
251
|
+
return result?.trace_id === undefined ? undefined : result
|
|
210
252
|
}
|
|
@@ -12,6 +12,7 @@ const { version } = require('../../../../../package.json')
|
|
|
12
12
|
|
|
13
13
|
module.exports = send
|
|
14
14
|
|
|
15
|
+
const MAX_MESSAGE_LENGTH = 8 * 1024 // 8KB
|
|
15
16
|
const MAX_LOG_PAYLOAD_SIZE = 1024 * 1024 // 1MB
|
|
16
17
|
|
|
17
18
|
const ddsource = 'dd_debugger'
|
|
@@ -36,7 +37,9 @@ function send (message, logger, dd, snapshot) {
|
|
|
36
37
|
ddsource,
|
|
37
38
|
hostname,
|
|
38
39
|
service,
|
|
39
|
-
message
|
|
40
|
+
message: message?.length > MAX_MESSAGE_LENGTH
|
|
41
|
+
? message.slice(0, MAX_MESSAGE_LENGTH) + '…'
|
|
42
|
+
: message,
|
|
40
43
|
logger,
|
|
41
44
|
dd,
|
|
42
45
|
debugger: { snapshot }
|
|
@@ -27,7 +27,7 @@ const self = module.exports = {
|
|
|
27
27
|
|
|
28
28
|
async getGeneratedPosition (url, source, line, sourceMapURL) {
|
|
29
29
|
const dir = dirname(new URL(url).pathname)
|
|
30
|
-
return
|
|
30
|
+
return SourceMapConsumer.with(
|
|
31
31
|
await self.loadSourceMap(dir, sourceMapURL),
|
|
32
32
|
null,
|
|
33
33
|
(consumer) => consumer.generatedPositionFor({ source, line, column: 0 })
|