@kubesense/kubesense-browser-rum-core 1.3.0 → 1.5.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/README.md +8 -1
- package/cjs/boot/preStartRum.js +18 -0
- package/cjs/boot/preStartRum.js.map +1 -1
- package/cjs/domain/configuration/configuration.d.ts +31 -0
- package/cjs/domain/configuration/configuration.js +2 -0
- package/cjs/domain/configuration/configuration.js.map +1 -1
- package/cjs/domain/configuration/index.d.ts +2 -0
- package/cjs/domain/configuration/index.js +2 -0
- package/cjs/domain/configuration/index.js.map +1 -1
- package/cjs/domain/configuration/remoteSdkConfiguration.d.ts +18 -0
- package/cjs/domain/configuration/remoteSdkConfiguration.js +142 -0
- package/cjs/domain/configuration/remoteSdkConfiguration.js.map +1 -0
- package/cjs/domain/configuration/sessionReplayRoutes.d.ts +35 -0
- package/cjs/domain/configuration/sessionReplayRoutes.js +70 -0
- package/cjs/domain/configuration/sessionReplayRoutes.js.map +1 -0
- package/cjs/domain/contexts/defaultContext.js +1 -1
- package/cjs/index.d.ts +2 -1
- package/cjs/index.js +4 -1
- package/cjs/index.js.map +1 -1
- package/cjs/rumEvent.types.d.ts +4 -0
- package/esm/boot/preStartRum.js +19 -1
- package/esm/boot/preStartRum.js.map +1 -1
- package/esm/domain/configuration/configuration.d.ts +31 -0
- package/esm/domain/configuration/configuration.js +2 -0
- package/esm/domain/configuration/configuration.js.map +1 -1
- package/esm/domain/configuration/index.d.ts +2 -0
- package/esm/domain/configuration/index.js +2 -0
- package/esm/domain/configuration/index.js.map +1 -1
- package/esm/domain/configuration/remoteSdkConfiguration.d.ts +18 -0
- package/esm/domain/configuration/remoteSdkConfiguration.js +137 -0
- package/esm/domain/configuration/remoteSdkConfiguration.js.map +1 -0
- package/esm/domain/configuration/sessionReplayRoutes.d.ts +35 -0
- package/esm/domain/configuration/sessionReplayRoutes.js +65 -0
- package/esm/domain/configuration/sessionReplayRoutes.js.map +1 -0
- package/esm/domain/contexts/defaultContext.js +1 -1
- package/esm/index.d.ts +2 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- package/esm/rumEvent.types.d.ts +4 -0
- package/package.json +13 -3
- package/src/boot/preStartRum.ts +23 -0
- package/src/domain/configuration/configuration.ts +35 -0
- package/src/domain/configuration/index.ts +2 -0
- package/src/domain/configuration/remoteSdkConfiguration.ts +190 -0
- package/src/domain/configuration/sessionReplayRoutes.ts +96 -0
- package/src/index.ts +2 -0
- package/src/rumEvent.types.ts +4 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { RemoteSdkConfig } from '@kubesense/kubesense-browser-core'
|
|
2
|
+
import {
|
|
3
|
+
DefaultPrivacyLevel,
|
|
4
|
+
TraceContextInjection,
|
|
5
|
+
getCachedSdkConfig,
|
|
6
|
+
readBoolean,
|
|
7
|
+
readEnum,
|
|
8
|
+
readSampleRate,
|
|
9
|
+
readString,
|
|
10
|
+
refreshSdkConfig,
|
|
11
|
+
} from '@kubesense/kubesense-browser-core'
|
|
12
|
+
import type { RumInitConfiguration } from './configuration'
|
|
13
|
+
import type { SessionReplayRoutes } from './sessionReplayRoutes'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Applies the dashboard-managed configuration document to the user's init configuration.
|
|
17
|
+
*
|
|
18
|
+
* Only scalar settings travel: anything that is a function or a matcher (`beforeSend`,
|
|
19
|
+
* `allowedTracingUrls`, `excludedActivityUrls`, plugins) stays in application code. Identity and
|
|
20
|
+
* endpoint settings (`clientToken`, `applicationId`, `kubesenseRumEndpoint`, `proxy`) are also
|
|
21
|
+
* excluded — they determine where the configuration request itself goes, and letting them be
|
|
22
|
+
* remotely rewritten would be a redirection risk.
|
|
23
|
+
*
|
|
24
|
+
* Every field is applied only when present and valid, so the user's own init configuration —
|
|
25
|
+
* and failing that, the SDK default — always wins over a missing or malformed remote value.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const PRIVACY_LEVELS = [
|
|
29
|
+
DefaultPrivacyLevel.ALLOW,
|
|
30
|
+
DefaultPrivacyLevel.MASK,
|
|
31
|
+
DefaultPrivacyLevel.MASK_USER_INPUT,
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
const TRACE_CONTEXT_INJECTIONS = [TraceContextInjection.ALL, TraceContextInjection.SAMPLED] as const
|
|
35
|
+
|
|
36
|
+
export interface RemoteSdkConfigurationResult {
|
|
37
|
+
initConfiguration: RumInitConfiguration
|
|
38
|
+
/** `false` when the dashboard has switched RUM off entirely. */
|
|
39
|
+
rumEnabled: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read the cached document and merge it into the init configuration. Synchronous and
|
|
44
|
+
* network-free: this runs on the initialization path.
|
|
45
|
+
*/
|
|
46
|
+
export function applyCachedSdkConfiguration(initConfiguration: RumInitConfiguration): RemoteSdkConfigurationResult {
|
|
47
|
+
const remoteConfig = getCachedSdkConfig()
|
|
48
|
+
if (!remoteConfig) {
|
|
49
|
+
return { initConfiguration, rumEnabled: true }
|
|
50
|
+
}
|
|
51
|
+
return applySdkConfiguration(initConfiguration, remoteConfig)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function applySdkConfiguration(
|
|
55
|
+
initConfiguration: RumInitConfiguration,
|
|
56
|
+
remoteConfig: RemoteSdkConfig
|
|
57
|
+
): RemoteSdkConfigurationResult {
|
|
58
|
+
const features = remoteConfig.features
|
|
59
|
+
if (readBoolean(features as { [key: string]: unknown } | undefined, 'rum') === false) {
|
|
60
|
+
return { initConfiguration, rumEnabled: false }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const applied: RumInitConfiguration = { ...initConfiguration }
|
|
64
|
+
|
|
65
|
+
applyRumSection(applied, remoteConfig.rum)
|
|
66
|
+
applySessionReplaySection(applied, remoteConfig.sessionReplay)
|
|
67
|
+
applyProfilingSection(applied, remoteConfig.profiling)
|
|
68
|
+
applyFeatureSwitches(applied, features)
|
|
69
|
+
|
|
70
|
+
return { initConfiguration: applied, rumEnabled: true }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function applyRumSection(applied: RumInitConfiguration, rum: { [key: string]: unknown } | undefined) {
|
|
74
|
+
if (!rum) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
assign(applied, 'sessionSampleRate', readSampleRate(rum, 'sessionSampleRate'))
|
|
79
|
+
assign(applied, 'telemetrySampleRate', readSampleRate(rum, 'telemetrySampleRate'))
|
|
80
|
+
assign(applied, 'traceSampleRate', readSampleRate(rum, 'traceSampleRate'))
|
|
81
|
+
|
|
82
|
+
assign(applied, 'trackUserInteractions', readBoolean(rum, 'trackUserInteractions'))
|
|
83
|
+
assign(applied, 'trackResources', readBoolean(rum, 'trackResources'))
|
|
84
|
+
assign(applied, 'trackLongTasks', readBoolean(rum, 'trackLongTasks'))
|
|
85
|
+
assign(applied, 'trackViewsManually', readBoolean(rum, 'trackViewsManually'))
|
|
86
|
+
assign(applied, 'trackBfcacheViews', readBoolean(rum, 'trackBfcacheViews'))
|
|
87
|
+
assign(applied, 'trackAnonymousUser', readBoolean(rum, 'trackAnonymousUser'))
|
|
88
|
+
assign(applied, 'trackSessionAcrossSubdomains', readBoolean(rum, 'trackSessionAcrossSubdomains'))
|
|
89
|
+
assign(applied, 'enablePrivacyForActionName', readBoolean(rum, 'enablePrivacyForActionName'))
|
|
90
|
+
assign(applied, 'propagateTraceBaggage', readBoolean(rum, 'propagateTraceBaggage'))
|
|
91
|
+
|
|
92
|
+
assign(applied, 'defaultPrivacyLevel', readEnum(rum, 'defaultPrivacyLevel', PRIVACY_LEVELS))
|
|
93
|
+
assign(applied, 'traceContextInjection', readEnum(rum, 'traceContextInjection', TRACE_CONTEXT_INJECTIONS))
|
|
94
|
+
assign(applied, 'actionNameAttribute', readString(rum, 'actionNameAttribute'))
|
|
95
|
+
|
|
96
|
+
assign(applied, 'service', readString(rum, 'service'))
|
|
97
|
+
assign(applied, 'env', readString(rum, 'env'))
|
|
98
|
+
assign(applied, 'version', readString(rum, 'version'))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function applySessionReplaySection(
|
|
102
|
+
applied: RumInitConfiguration,
|
|
103
|
+
sessionReplay: { [key: string]: unknown } | undefined
|
|
104
|
+
) {
|
|
105
|
+
if (!sessionReplay) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
assign(applied, 'sessionReplaySampleRate', readSampleRate(sessionReplay, 'sampleRate'))
|
|
110
|
+
assign(applied, 'startSessionReplayRecordingManually', readBoolean(sessionReplay, 'startRecordingManually'))
|
|
111
|
+
assign(applied, 'defaultPrivacyLevel', readEnum(sessionReplay, 'defaultPrivacyLevel', PRIVACY_LEVELS))
|
|
112
|
+
assign(applied, 'sessionReplayRoutes', readRoutes(sessionReplay.routes))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Route rules arriving as JSON can only be plain strings (prefix matches). Regexes and functions
|
|
117
|
+
* are deliberately not accepted from the dashboard: they cannot be expressed safely in JSON and a
|
|
118
|
+
* pathological pattern would run on every view change.
|
|
119
|
+
*/
|
|
120
|
+
function readRoutes(routes: unknown): SessionReplayRoutes | undefined {
|
|
121
|
+
if (!routes || typeof routes !== 'object' || Array.isArray(routes)) {
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const include = readStringList((routes as { include?: unknown }).include)
|
|
126
|
+
const exclude = readStringList((routes as { exclude?: unknown }).exclude)
|
|
127
|
+
|
|
128
|
+
if (!include && !exclude) {
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { include, exclude }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readStringList(value: unknown): string[] | undefined {
|
|
136
|
+
if (!Array.isArray(value)) {
|
|
137
|
+
return undefined
|
|
138
|
+
}
|
|
139
|
+
const strings = value.filter((item): item is string => typeof item === 'string' && item.length > 0)
|
|
140
|
+
return strings.length > 0 ? strings : undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function applyProfilingSection(applied: RumInitConfiguration, profiling: { [key: string]: unknown } | undefined) {
|
|
144
|
+
if (!profiling) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
assign(applied, 'profilingSampleRate', readSampleRate(profiling, 'sampleRate'))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Feature switches are applied last so they always win over the per-section sample rates above.
|
|
152
|
+
* Turning a feature off is expressed through the setting that stops all of its collection.
|
|
153
|
+
*/
|
|
154
|
+
function applyFeatureSwitches(applied: RumInitConfiguration, features: RemoteSdkConfig['features']) {
|
|
155
|
+
const source = features as { [key: string]: unknown } | undefined
|
|
156
|
+
|
|
157
|
+
if (readBoolean(source, 'sessionReplay') === false) {
|
|
158
|
+
applied.sessionReplaySampleRate = 0
|
|
159
|
+
applied.startSessionReplayRecordingManually = true
|
|
160
|
+
// Drop any route rules too: with replay off they are dead weight, and leaving them behind
|
|
161
|
+
// would suggest recording still happens somewhere.
|
|
162
|
+
applied.sessionReplayRoutes = undefined
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (readBoolean(source, 'profiling') === false) {
|
|
166
|
+
applied.profilingSampleRate = 0
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Assigning only defined values keeps the "absent means keep the local value" guarantee in one
|
|
172
|
+
* place instead of at every call site.
|
|
173
|
+
*/
|
|
174
|
+
function assign<K extends keyof RumInitConfiguration>(
|
|
175
|
+
target: RumInitConfiguration,
|
|
176
|
+
key: K,
|
|
177
|
+
value: RumInitConfiguration[K] | undefined
|
|
178
|
+
) {
|
|
179
|
+
if (value !== undefined) {
|
|
180
|
+
target[key] = value
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Refresh the cache for the next page load. Must be called *after* the SDK has started so it
|
|
186
|
+
* never delays initialization.
|
|
187
|
+
*/
|
|
188
|
+
export function scheduleSdkConfigurationRefresh(initConfiguration: RumInitConfiguration) {
|
|
189
|
+
return refreshSdkConfig(initConfiguration, initConfiguration.applicationId)
|
|
190
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { MatchOption } from '@kubesense/kubesense-browser-core'
|
|
2
|
+
import { display, isMatchOption, matchList } from '@kubesense/kubesense-browser-core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Route-scoped Session Replay.
|
|
6
|
+
*
|
|
7
|
+
* Recording is normally decided once per session by `sessionReplaySampleRate`. These rules add a
|
|
8
|
+
* second, per-view gate so a session that *is* eligible only gets recorded while the user is on
|
|
9
|
+
* a matching route.
|
|
10
|
+
*
|
|
11
|
+
* Rules can only ever narrow recording, never widen it: a session that lost the replay sampling
|
|
12
|
+
* draw is never recorded, whatever the routes say.
|
|
13
|
+
*/
|
|
14
|
+
export interface SessionReplayRoutes {
|
|
15
|
+
/** Record only on these routes. When omitted, every route is eligible. */
|
|
16
|
+
include?: MatchOption[] | undefined
|
|
17
|
+
/** Never record on these routes. Takes precedence over `include`. */
|
|
18
|
+
exclude?: MatchOption[] | undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Rules are matched against the **path** — `pathname` + `search` + `hash` — not the full URL, so
|
|
23
|
+
* a dashboard entry reads the way a route is written (`/checkout`). String options match by
|
|
24
|
+
* prefix, so `/checkout` also covers `/checkout/payment`.
|
|
25
|
+
*/
|
|
26
|
+
export function getRoutePath(location: Location | URL = window.location): string {
|
|
27
|
+
return `${location.pathname}${location.search}${location.hash}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns whether recording is allowed on the given path.
|
|
32
|
+
*
|
|
33
|
+
* No rules at all means "record everywhere", which keeps the behaviour of an SDK that has never
|
|
34
|
+
* been configured with routes.
|
|
35
|
+
*/
|
|
36
|
+
export function isRouteRecordable(routes: SessionReplayRoutes | undefined, path: string): boolean {
|
|
37
|
+
if (!routes) {
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Exclusions win, so a route can always be carved out of a broad `include`.
|
|
42
|
+
if (routes.exclude && routes.exclude.length > 0 && matchList(routes.exclude, path, true)) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (routes.include && routes.include.length > 0) {
|
|
47
|
+
return matchList(routes.include, path, true)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validates the user-provided option. An invalid entry is dropped rather than silently treated
|
|
55
|
+
* as "match nothing", which would turn a typo into a total loss of replay.
|
|
56
|
+
*/
|
|
57
|
+
export function validateSessionReplayRoutes(routes: unknown): SessionReplayRoutes | undefined {
|
|
58
|
+
if (routes === undefined || routes === null) {
|
|
59
|
+
return undefined
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof routes !== 'object' || Array.isArray(routes)) {
|
|
63
|
+
display.error('Session Replay routes should be an object with optional include and exclude arrays')
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const include = validateMatchOptionList((routes as SessionReplayRoutes).include, 'include')
|
|
68
|
+
const exclude = validateMatchOptionList((routes as SessionReplayRoutes).exclude, 'exclude')
|
|
69
|
+
|
|
70
|
+
if (!include && !exclude) {
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { include, exclude }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function validateMatchOptionList(list: unknown, name: string): MatchOption[] | undefined {
|
|
78
|
+
if (list === undefined) {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!Array.isArray(list)) {
|
|
83
|
+
display.error(`Session Replay routes "${name}" should be an array`)
|
|
84
|
+
return undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const validOptions = list.filter((item) => {
|
|
88
|
+
if (isMatchOption(item)) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
display.error(`Session Replay routes "${name}" entries should be a string, RegExp or function`)
|
|
92
|
+
return false
|
|
93
|
+
}) as MatchOption[]
|
|
94
|
+
|
|
95
|
+
return validOptions.length > 0 ? validOptions : undefined
|
|
96
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -48,7 +48,9 @@ export type {
|
|
|
48
48
|
RumConfiguration,
|
|
49
49
|
FeatureFlagsForEvents,
|
|
50
50
|
RemoteConfiguration,
|
|
51
|
+
SessionReplayRoutes,
|
|
51
52
|
} from './domain/configuration'
|
|
53
|
+
export { isRouteRecordable, getRoutePath } from './domain/configuration'
|
|
52
54
|
export { DEFAULT_PROGRAMMATIC_ACTION_NAME_ATTRIBUTE } from './domain/action/actionNameConstants'
|
|
53
55
|
export { STABLE_ATTRIBUTES } from './domain/getSelectorFromElement'
|
|
54
56
|
export * from './browser/htmlDomUtils'
|
package/src/rumEvent.types.ts
CHANGED
|
@@ -1519,6 +1519,10 @@ export interface CommonProperties {
|
|
|
1519
1519
|
* Email of the user
|
|
1520
1520
|
*/
|
|
1521
1521
|
readonly email?: string
|
|
1522
|
+
/**
|
|
1523
|
+
* Mobile phone number of the user
|
|
1524
|
+
*/
|
|
1525
|
+
readonly mobile?: string
|
|
1522
1526
|
/**
|
|
1523
1527
|
* Identifier of the user across sessions
|
|
1524
1528
|
*/
|