@kubesense/kubesense-browser-core 1.4.0 → 1.6.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.
Files changed (60) hide show
  1. package/README.md +40 -15
  2. package/cjs/boot/init.js +1 -1
  3. package/cjs/domain/configuration/configuration.d.ts +3 -3
  4. package/cjs/domain/configuration/configuration.js +3 -3
  5. package/cjs/domain/configuration/configuration.js.map +1 -1
  6. package/cjs/domain/configuration/endpointBuilder.js +1 -1
  7. package/cjs/domain/configuration/index.d.ts +2 -0
  8. package/cjs/domain/configuration/index.js +14 -1
  9. package/cjs/domain/configuration/index.js.map +1 -1
  10. package/cjs/domain/configuration/remoteSdkConfig.d.ts +81 -0
  11. package/cjs/domain/configuration/remoteSdkConfig.js +185 -0
  12. package/cjs/domain/configuration/remoteSdkConfig.js.map +1 -0
  13. package/cjs/domain/contexts/userContext.d.ts +1 -0
  14. package/cjs/domain/contexts/userContext.js +4 -0
  15. package/cjs/domain/contexts/userContext.js.map +1 -1
  16. package/cjs/domain/tags.js +1 -1
  17. package/cjs/domain/telemetry/telemetry.js +1 -1
  18. package/cjs/index.d.ts +2 -1
  19. package/cjs/index.js +15 -3
  20. package/cjs/index.js.map +1 -1
  21. package/cjs/tools/display.js +1 -1
  22. package/cjs/tools/display.js.map +1 -1
  23. package/cjs/transport/eventBridge.d.ts +2 -2
  24. package/cjs/transport/eventBridge.js +1 -1
  25. package/cjs/transport/eventBridge.js.map +1 -1
  26. package/cjs/transport/index.d.ts +1 -1
  27. package/esm/boot/init.js +1 -1
  28. package/esm/domain/configuration/configuration.d.ts +3 -3
  29. package/esm/domain/configuration/configuration.js +3 -3
  30. package/esm/domain/configuration/configuration.js.map +1 -1
  31. package/esm/domain/configuration/endpointBuilder.js +1 -1
  32. package/esm/domain/configuration/index.d.ts +2 -0
  33. package/esm/domain/configuration/index.js +1 -0
  34. package/esm/domain/configuration/index.js.map +1 -1
  35. package/esm/domain/configuration/remoteSdkConfig.d.ts +81 -0
  36. package/esm/domain/configuration/remoteSdkConfig.js +172 -0
  37. package/esm/domain/configuration/remoteSdkConfig.js.map +1 -0
  38. package/esm/domain/contexts/userContext.d.ts +1 -0
  39. package/esm/domain/contexts/userContext.js +4 -0
  40. package/esm/domain/contexts/userContext.js.map +1 -1
  41. package/esm/domain/tags.js +1 -1
  42. package/esm/domain/telemetry/telemetry.js +1 -1
  43. package/esm/index.d.ts +2 -1
  44. package/esm/index.js +1 -1
  45. package/esm/index.js.map +1 -1
  46. package/esm/tools/display.js +1 -1
  47. package/esm/tools/display.js.map +1 -1
  48. package/esm/transport/eventBridge.d.ts +2 -2
  49. package/esm/transport/eventBridge.js +1 -1
  50. package/esm/transport/eventBridge.js.map +1 -1
  51. package/esm/transport/index.d.ts +1 -1
  52. package/package.json +2 -2
  53. package/src/domain/configuration/configuration.ts +19 -19
  54. package/src/domain/configuration/index.ts +15 -0
  55. package/src/domain/configuration/remoteSdkConfig.ts +213 -0
  56. package/src/domain/contexts/userContext.ts +5 -0
  57. package/src/index.ts +13 -0
  58. package/src/tools/display.ts +1 -1
  59. package/src/transport/eventBridge.ts +3 -3
  60. package/src/transport/index.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kubesense/kubesense-browser-core",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Core utilities shared across the Kubesense Browser SDK (session management, transport, configuration, and context).",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -35,5 +35,5 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "gitHead": "af1a0950e2441f45b4303b2dc17b09f9cd0d999d"
38
+ "gitHead": "5b90a5a0ea4ee972c079b9a55c45d05c44f14688"
39
39
  }
@@ -47,27 +47,27 @@ export type TraceContextInjection = (typeof TraceContextInjection)[keyof typeof
47
47
 
48
48
  export interface InitConfiguration {
49
49
  /**
50
- * The client token for Kubsesense. Required for authenticating your application with Kubsesense.
51
- *
50
+ * The client token for KubeSense. Required for authenticating your application with KubeSense.
51
+ *
52
52
  * @category Authentication
53
53
  */
54
54
  clientToken: string
55
55
  /**
56
- * A callback function that can be used to modify events before they are sent to Kubsesense.
57
- *
56
+ * A callback function that can be used to modify events before they are sent to KubeSense.
57
+ *
58
58
  * @category Data Collection
59
59
  */
60
60
  beforeSend?: GenericBeforeSendCallback | undefined
61
61
  /**
62
62
  * The percentage of sessions tracked. A value between 0 and 100.
63
- *
63
+ *
64
64
  * @category Data Collection
65
65
  * @defaultValue 100
66
66
  */
67
67
  sessionSampleRate?: number | undefined
68
68
  /**
69
69
  * The percentage of telemetry events sent. A value between 0 and 100.
70
- *
70
+ *
71
71
  * @category Data Collection
72
72
  * @defaultValue 20
73
73
  */
@@ -82,7 +82,7 @@ export interface InitConfiguration {
82
82
  * Which storage strategy to use for persisting sessions. Can be either 'cookie' or 'local-storage'.
83
83
  *
84
84
  * Important: If you are using the RUM and Logs Browser SDKs, this option must be configured with identical values
85
- *
85
+ *
86
86
  * @category Session Persistence
87
87
  * @defaultValue "cookie"
88
88
  */
@@ -109,7 +109,7 @@ export interface InitConfiguration {
109
109
  storeContextsAcrossPages?: boolean | undefined
110
110
  /**
111
111
  * Set the initial user tracking consent state.
112
- *
112
+ *
113
113
  * @category Privacy
114
114
  * @defaultValue granted
115
115
  */
@@ -125,12 +125,12 @@ export interface InitConfiguration {
125
125
  // transport options
126
126
  /**
127
127
  * Optional proxy URL, for example: https://www.proxy.com/path.
128
- *
128
+ *
129
129
  * @category Transport
130
130
  */
131
131
  proxy?: string | ProxyFn | undefined
132
132
  /**
133
- *
133
+ *
134
134
  * @category Transport
135
135
  * @defaultValue rum.kubesense.ai
136
136
  */
@@ -138,20 +138,20 @@ export interface InitConfiguration {
138
138
 
139
139
  // tag and context options
140
140
  /**
141
- * The service name for your application.
142
- *
141
+ * The service name for your application.
142
+ *
143
143
  * @category Data Collection
144
144
  */
145
145
  service?: string | undefined | null
146
146
  /**
147
147
  * The application’s environment, for example: prod, pre-prod, and staging.
148
- *
148
+ *
149
149
  * @category Data Collection
150
150
  */
151
151
  env?: string | undefined | null
152
152
  /**
153
- * The application’s version, for example: 1.2.3, 6c44da20, and 2020.02.13.
154
- *
153
+ * The application’s version, for example: 1.2.3, 6c44da20, and 2020.02.13.
154
+ *
155
155
  * @category Data Collection
156
156
  */
157
157
  version?: string | undefined | null
@@ -212,7 +212,7 @@ export interface InitConfiguration {
212
212
  */
213
213
  datacenter?: string
214
214
  /**
215
- * [Internal option] Kubsesense internal analytics subdomain
215
+ * [Internal option] KubeSense internal analytics subdomain
216
216
  * @internal
217
217
  */
218
218
  // TODO next major: remove this option and replace usages by proxyFn
@@ -299,9 +299,9 @@ function isString(tag: unknown, tagName: string): tag is string | undefined | nu
299
299
  return true
300
300
  }
301
301
 
302
- function isKubsesenseSite(site: unknown) {
302
+ function isKubeSenseSite(site: unknown) {
303
303
  if (site && typeof site === 'string' && !/(kubesense|tyke)/.test(site)) {
304
- display.error(`Site should be a valid Kubsesense site. ${MORE_DETAILS} ${DOCS_ORIGIN}/getting_started/site/.`)
304
+ display.error(`Site should be a valid KubeSense site. ${MORE_DETAILS} ${DOCS_ORIGIN}/getting_started/site/.`)
305
305
  return false
306
306
  }
307
307
  return true
@@ -333,7 +333,7 @@ export function validateAndBuildConfiguration(
333
333
  }
334
334
 
335
335
  if (
336
- // !isKubsesenseSite(initConfiguration.kubesenseRumEndpoint) ||
336
+ // !isKubeSenseSite(initConfiguration.kubesenseRumEndpoint) ||
337
337
  !isSampleRate(initConfiguration.sessionSampleRate, 'Session') ||
338
338
  !isSampleRate(initConfiguration.telemetrySampleRate, 'Telemetry') ||
339
339
  !isSampleRate(initConfiguration.telemetryConfigurationSampleRate, 'Telemetry Configuration') ||
@@ -8,5 +8,20 @@ export {
8
8
  } from './configuration'
9
9
  export type { EndpointBuilder, TrackType } from './endpointBuilder'
10
10
  export { createEndpointBuilder, buildEndpointHost } from './endpointBuilder'
11
+ export type { RemoteSdkConfig, RemoteSdkConfigFeatures } from './remoteSdkConfig'
12
+ export {
13
+ SDK_CONFIG_CACHE_KEY,
14
+ SDK_CONFIG_PATH,
15
+ buildSdkConfigEndpoint,
16
+ clearCachedSdkConfig,
17
+ getCachedSdkConfig,
18
+ parseSdkConfig,
19
+ readBoolean,
20
+ readEnum,
21
+ readSampleRate,
22
+ readString,
23
+ refreshSdkConfig,
24
+ setCachedSdkConfig,
25
+ } from './remoteSdkConfig'
11
26
  export * from '../intakeSites'
12
27
  export { computeTransportConfiguration, isIntakeUrl } from './transportConfiguration'
@@ -0,0 +1,213 @@
1
+ import { generateUUID } from '../../tools/utils/stringUtils'
2
+ import { display } from '../../tools/display'
3
+ import { monitorError } from '../../tools/monitor'
4
+ import type { InitConfiguration } from './configuration'
5
+ import { buildEndpointHost } from './endpointBuilder'
6
+
7
+ /**
8
+ * Dashboard-managed SDK settings, served by the Kubesense collector.
9
+ *
10
+ * The document is fetched asynchronously *after* the SDK has started and cached in local
11
+ * storage, so it costs nothing on the initialization path. Cached values are applied on the
12
+ * next page load — SDK configuration is frozen at `init()` time by design.
13
+ */
14
+
15
+ export const SDK_CONFIG_CACHE_KEY = '_kubesense_sdk_config'
16
+ export const SDK_CONFIG_PATH = '/rum/api/v1/sdk-config'
17
+
18
+ /** Give up rather than let a hung request keep a page-load handle alive. */
19
+ export const SDK_CONFIG_FETCH_TIMEOUT = 10_000
20
+
21
+ /**
22
+ * A remote configuration document. Every field is optional: a missing (or invalid) field means
23
+ * "leave the SDK default alone", so a malformed document can never take an application away
24
+ * from its defaults.
25
+ */
26
+ export interface RemoteSdkConfig {
27
+ /** Free-form number for dashboard bookkeeping. Not interpreted by the SDK. */
28
+ version?: number
29
+ features?: RemoteSdkConfigFeatures
30
+ rum?: { [key: string]: unknown }
31
+ sessionReplay?: { [key: string]: unknown }
32
+ profiling?: { [key: string]: unknown }
33
+ logs?: { [key: string]: unknown }
34
+ [key: string]: unknown
35
+ }
36
+
37
+ export interface RemoteSdkConfigFeatures {
38
+ rum?: boolean
39
+ sessionReplay?: boolean
40
+ profiling?: boolean
41
+ logs?: boolean
42
+ }
43
+
44
+ /**
45
+ * Read the cached document. Synchronous, never touches the network, and never throws: a
46
+ * corrupted or unavailable cache resolves to `undefined` (pure SDK defaults).
47
+ */
48
+ export function getCachedSdkConfig(): RemoteSdkConfig | undefined {
49
+ let raw: string | null
50
+ try {
51
+ raw = localStorage.getItem(SDK_CONFIG_CACHE_KEY)
52
+ } catch {
53
+ // Local storage can be unavailable (disabled, quota, sandboxed iframe).
54
+ return undefined
55
+ }
56
+
57
+ if (!raw) {
58
+ return undefined
59
+ }
60
+
61
+ const config = parseSdkConfig(raw)
62
+ if (!config) {
63
+ // Drop a document we can no longer read so we stop paying for it on every page load.
64
+ clearCachedSdkConfig()
65
+ }
66
+ return config
67
+ }
68
+
69
+ export function setCachedSdkConfig(rawConfig: string) {
70
+ try {
71
+ localStorage.setItem(SDK_CONFIG_CACHE_KEY, rawConfig)
72
+ } catch {
73
+ // Ignore: caching is an optimization, not a requirement.
74
+ }
75
+ }
76
+
77
+ export function clearCachedSdkConfig() {
78
+ try {
79
+ localStorage.removeItem(SDK_CONFIG_CACHE_KEY)
80
+ } catch {
81
+ // Ignore.
82
+ }
83
+ }
84
+
85
+ /**
86
+ * A document is only usable if it is a JSON object. Anything else (array, primitive, invalid
87
+ * JSON) is rejected wholesale.
88
+ */
89
+ export function parseSdkConfig(raw: string): RemoteSdkConfig | undefined {
90
+ try {
91
+ const parsed: unknown = JSON.parse(raw)
92
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
93
+ return undefined
94
+ }
95
+ return parsed as RemoteSdkConfig
96
+ } catch {
97
+ return undefined
98
+ }
99
+ }
100
+
101
+ /**
102
+ * The config endpoint deliberately carries the same query parameters as the intake, for two
103
+ * reasons: it authenticates without a custom header (which would force a CORS preflight), and
104
+ * it makes `isIntakeUrl()` return true, so the SDK never reports its own configuration request
105
+ * as a resource or a network error.
106
+ */
107
+ export function buildSdkConfigEndpoint(initConfiguration: InitConfiguration, applicationId?: string): string {
108
+ const host = buildEndpointHost('rum', initConfiguration)
109
+ const parameters = [
110
+ 'ksource=browser',
111
+ `kubesense-api-key=${encodeURIComponent(initConfiguration.clientToken)}`,
112
+ `kubesense-request-id=${generateUUID()}`,
113
+ ]
114
+ if (applicationId) {
115
+ // The collector resolves the application from this parameter first, falling back to the
116
+ // token -> application mapping. Sending it makes per-application configuration explicit.
117
+ parameters.push(`application_id=${encodeURIComponent(applicationId)}`)
118
+ }
119
+ return `https://${host}${SDK_CONFIG_PATH}?${parameters.join('&')}`
120
+ }
121
+
122
+ /**
123
+ * Fetch the document and refresh the cache for the *next* page load. Fire-and-forget: any
124
+ * failure (offline, 404 because the collector has no config file, 401, timeout, malformed
125
+ * body) leaves the existing cache untouched.
126
+ *
127
+ * The response is validated before it overwrites the cache, so a half-written document on the
128
+ * collector can never poison a client.
129
+ */
130
+ export function refreshSdkConfig(initConfiguration: InitConfiguration, applicationId?: string): Promise<void> {
131
+ if (typeof fetch !== 'function') {
132
+ return Promise.resolve()
133
+ }
134
+
135
+ let endpoint: string
136
+ try {
137
+ endpoint = buildSdkConfigEndpoint(initConfiguration, applicationId)
138
+ } catch (error) {
139
+ monitorError(error)
140
+ return Promise.resolve()
141
+ }
142
+
143
+ return fetch(endpoint, {
144
+ method: 'GET',
145
+ // Configuration is not user data: never attach cookies to it.
146
+ credentials: 'omit',
147
+ mode: 'cors',
148
+ // Always ask the collector, never a proxy cache — dashboard edits must propagate.
149
+ cache: 'no-store',
150
+ signal: createTimeoutSignal(SDK_CONFIG_FETCH_TIMEOUT),
151
+ })
152
+ .then((response) => {
153
+ if (!response.ok) {
154
+ // 404 simply means "no configuration published"; keep whatever we already had.
155
+ return undefined
156
+ }
157
+ return response.text()
158
+ })
159
+ .then((rawConfig) => {
160
+ if (rawConfig === undefined) {
161
+ return
162
+ }
163
+ if (!parseSdkConfig(rawConfig)) {
164
+ display.warn('Remote SDK configuration is not a valid JSON object, ignoring it.')
165
+ return
166
+ }
167
+ setCachedSdkConfig(rawConfig)
168
+ })
169
+ .catch(() => {
170
+ // Network failures are expected and must stay silent: the SDK keeps running on the
171
+ // cached configuration, or on its defaults.
172
+ })
173
+ }
174
+
175
+ function createTimeoutSignal(timeout: number): AbortSignal | undefined {
176
+ // AbortSignal.timeout is not available on all supported browsers; without it the fetch
177
+ // simply runs to completion, which is acceptable for a fire-and-forget request.
178
+ if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
179
+ return AbortSignal.timeout(timeout)
180
+ }
181
+ return undefined
182
+ }
183
+
184
+ /* ------------------------------------------------------------------------------------------
185
+ * Defensive readers
186
+ *
187
+ * Each returns `undefined` when the value is missing or unusable, which callers translate into
188
+ * "do not override the SDK default".
189
+ * ---------------------------------------------------------------------------------------- */
190
+
191
+ export function readBoolean(source: { [key: string]: unknown } | undefined, key: string): boolean | undefined {
192
+ const value = source?.[key]
193
+ return typeof value === 'boolean' ? value : undefined
194
+ }
195
+
196
+ export function readSampleRate(source: { [key: string]: unknown } | undefined, key: string): number | undefined {
197
+ const value = source?.[key]
198
+ return typeof value === 'number' && isFinite(value) && value >= 0 && value <= 100 ? value : undefined
199
+ }
200
+
201
+ export function readString(source: { [key: string]: unknown } | undefined, key: string): string | undefined {
202
+ const value = source?.[key]
203
+ return typeof value === 'string' && value.length > 0 ? value : undefined
204
+ }
205
+
206
+ export function readEnum<T extends string>(
207
+ source: { [key: string]: unknown } | undefined,
208
+ key: string,
209
+ allowedValues: readonly T[]
210
+ ): T | undefined {
211
+ const value = source?.[key]
212
+ return typeof value === 'string' && (allowedValues as readonly string[]).includes(value) ? (value as T) : undefined
213
+ }
@@ -16,6 +16,7 @@ export interface User {
16
16
  id?: string | undefined
17
17
  email?: string | undefined
18
18
  name?: string | undefined
19
+ mobile?: string | undefined
19
20
  [key: string]: unknown
20
21
  }
21
22
 
@@ -143,6 +144,10 @@ export function buildUserContextManager() {
143
144
  id: { type: 'string' },
144
145
  name: { type: 'string' },
145
146
  email: { type: 'string' },
147
+ // Coerced to string like the other identity fields: phone numbers are
148
+ // routinely passed as JS numbers, and the collector unmarshals
149
+ // usr.mobile into a string column.
150
+ mobile: { type: 'string' },
146
151
  },
147
152
  })
148
153
  }
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type { Configuration, InitConfiguration, EndpointBuilder, ProxyFn } from './domain/configuration'
2
+ export type { RemoteSdkConfig, RemoteSdkConfigFeatures } from './domain/configuration'
2
3
  export {
3
4
  validateAndBuildConfiguration,
4
5
  DefaultPrivacyLevel,
@@ -10,6 +11,18 @@ export {
10
11
  INTAKE_SITE_PROD,
11
12
  INTAKE_SITE_STAGING,
12
13
  isIntakeUrl,
14
+ SDK_CONFIG_CACHE_KEY,
15
+ SDK_CONFIG_PATH,
16
+ buildSdkConfigEndpoint,
17
+ clearCachedSdkConfig,
18
+ getCachedSdkConfig,
19
+ parseSdkConfig,
20
+ readBoolean,
21
+ readEnum,
22
+ readSampleRate,
23
+ readString,
24
+ refreshSdkConfig,
25
+ setCachedSdkConfig,
13
26
  } from './domain/configuration'
14
27
  export * from './domain/intakeSites'
15
28
  export type { TrackingConsentState } from './domain/trackingConsent'
@@ -41,7 +41,7 @@ Object.keys(ConsoleApiName).forEach((name) => {
41
41
  originalConsoleMethods[name as ConsoleApiName] = globalConsole[name as ConsoleApiName]
42
42
  })
43
43
 
44
- const PREFIX = 'Kubsesense Browser SDK:'
44
+ const PREFIX = 'KubeSense Browser SDK:'
45
45
 
46
46
  export const display: Display = {
47
47
  debug: originalConsoleMethods.debug.bind(globalConsole, PREFIX),
@@ -2,10 +2,10 @@ import { getGlobalObject } from '../tools/globalObject'
2
2
  import type { DefaultPrivacyLevel } from '../domain/configuration'
3
3
 
4
4
  export interface BrowserWindowWithEventBridge extends Window {
5
- KubsesenseEventBridge?: KubsesenseEventBridge
5
+ KubeSenseEventBridge?: KubeSenseEventBridge
6
6
  }
7
7
 
8
- export interface KubsesenseEventBridge {
8
+ export interface KubeSenseEventBridge {
9
9
  getCapabilities?(): string
10
10
  getPrivacyLevel?(): DefaultPrivacyLevel
11
11
  getAllowedWebViewHosts(): string
@@ -56,5 +56,5 @@ export function canUseEventBridge(currentHost = getGlobalObject<Window>().locati
56
56
  }
57
57
 
58
58
  function getEventBridgeGlobal() {
59
- return getGlobalObject<BrowserWindowWithEventBridge>().KubsesenseEventBridge
59
+ return getGlobalObject<BrowserWindowWithEventBridge>().KubeSenseEventBridge
60
60
  }
@@ -1,6 +1,6 @@
1
1
  export type { BandwidthStats, HttpRequest, HttpRequestEvent, Payload, RetryInfo } from './httpRequest'
2
2
  export { createHttpRequest } from './httpRequest'
3
- export type { BrowserWindowWithEventBridge, KubsesenseEventBridge } from './eventBridge'
3
+ export type { BrowserWindowWithEventBridge, KubeSenseEventBridge } from './eventBridge'
4
4
  export { canUseEventBridge, bridgeSupports, getEventBridge, BridgeCapability } from './eventBridge'
5
5
  export { createBatch } from './batch'
6
6
  export type { FlushController, FlushEvent, FlushReason } from './flushController'