Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sdk/highlight-run/src/api/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,6 @@ export interface Observe {
environmentMetadata: LDPluginEnvironmentMetadata,
): void
getHooks?(metadata: LDPluginEnvironmentMetadata): Hook[]
setLDContextKeys(contextKeys: Attributes): void
getLDContextKeyAttributes(): Attributes | undefined
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (naming): why is it getLDContextKeyAttributes instead of getLDContextKeys? Or should we call the other one setLDContextKeyAttributes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can look into changing this - annoyingly we have to map through the entries to prefix with PRODUCT_ANALYTICS_CONTEXT_ATTR which this function does, so the data in != data out, but maybe not the best place to do so

}
27 changes: 26 additions & 1 deletion sdk/highlight-run/src/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ import { CustomSampler } from './otel/sampling/CustomSampler'
import randomUuidV4 from './utils/randomUuidV4'
import { LDContext } from '@launchdarkly/js-client-sdk'
import { MaskInputOptions } from './types/record'
import { ProductAnalyticsEvents } from 'client/types/observe'

export const HighlightWarning = (context: string, msg: any) => {
console.warn(`Highlight Warning: (${context}): `, { output: msg })
Expand Down Expand Up @@ -174,6 +175,7 @@ export type HighlightClassOptions = {
sendMode?: 'webworker' | 'local'
otlpEndpoint?: HighlightOptions['otlpEndpoint']
otel?: HighlightOptions['otel']
productAnalytics?: boolean | ProductAnalyticsEvents
contextFriendlyName?: (context: LDContext) => string | undefined
}

Expand Down Expand Up @@ -634,8 +636,8 @@ export class Highlight {
serviceName:
this.options?.serviceName ?? 'highlight-browser',
instrumentations: this.options?.otel?.instrumentations,
eventNames: this.options?.otel?.eventNames,
getIntegrations: () => [...this._integrations],
productAnalyticsEvents: this._productAnalyticsEvents(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double PathListener causes duplicate Navigate custom events

Medium Severity

In client/index.tsx, setupBrowserTracing now receives productAnalyticsEvents (defaulting to all enabled), which causes a new LocationChangeInstrumentation to be registered. This instrumentation uses PathListener, which patches history.pushState/replaceState and dispatches locationchange events. But client/index.tsx already registers its own PathListener at line 1072. Two PathListener instances each wrap history methods, so every navigation dispatches locationchange twice. The LocationChangeInstrumentation deduplicates via _lastUrl, but the existing navigate callback has no dedup, producing duplicate "Navigate" custom events.

Additional Locations (1)
Fix in Cursor Fix in Web

},
sampler,
)
Expand Down Expand Up @@ -935,6 +937,29 @@ SessionSecureID: ${this.sessionData.sessionSecureID}`,
}
}

private _productAnalyticsEvents(): ProductAnalyticsEvents {
const pa = this.options?.productAnalytics
if (pa === false) {
return {}
}

const paEvents = {
clicks: true,
pageViews: true,
trackEvents: true,
}
if (pa === undefined || pa === true) {
return paEvents
}

for (const event of Object.keys(pa)) {
if (pa[event as keyof ProductAnalyticsEvents] === false) {
paEvents[event as keyof ProductAnalyticsEvents] = false
}
}
return paEvents
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated _productAnalyticsEvents method across two files

Medium Severity

The _productAnalyticsEvents() method is identically duplicated in both client/index.tsx and sdk/observe.ts. This duplication increases the risk that a bug fix (like the productAnalytics: false issue) gets applied in one location but missed in the other. Extracting this into a shared utility would avoid that.

Additional Locations (1)
Fix in Cursor Fix in Web


async _visibilityHandler(hidden: boolean) {
if (this.manualStopped) {
this.logger.log(`Ignoring visibility event due to manual stop.`)
Expand Down
10 changes: 6 additions & 4 deletions sdk/highlight-run/src/client/otel/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as api from '@opentelemetry/api'
import { Context, Span } from '@opentelemetry/api'
import { Attributes, Context, Span } from '@opentelemetry/api'
import {
CompositePropagator,
W3CBaggagePropagator,
Expand Down Expand Up @@ -59,7 +59,7 @@ import version from '../../version'

import { ExportSampler } from './sampling/ExportSampler'
import { getPersistentSessionSecureID } from '../utils/sessionStorage/highlightSession'
import type { EventName } from '@opentelemetry/instrumentation-user-interaction'
import { ProductAnalyticsEvents } from 'client/types/observe'

export type Callback = (span?: Span) => any

Expand All @@ -74,9 +74,10 @@ export type BrowserTracingConfig = {
serviceVersion?: string
tracingOrigins?: boolean | (string | RegExp)[]
urlBlocklist?: string[]
eventNames?: EventName[]
instrumentations?: OtelInstrumentatonOptions
getIntegrations?: () => IntegrationClient[]
productAnalyticsEvents?: ProductAnalyticsEvents
getLDContextKeyAttributes?: () => Attributes | undefined
}

let providers: {
Expand Down Expand Up @@ -205,7 +206,8 @@ export const setupBrowserTracing = (
if (userInteractionConfig !== false) {
instrumentations.push(
new UserInteractionInstrumentation({
eventNames: config.eventNames,
productAnalyticsEvents: config.productAnalyticsEvents,
getLDContextKeyAttributes: config.getLDContextKeyAttributes,
}),
)
}
Expand Down
32 changes: 26 additions & 6 deletions sdk/highlight-run/src/client/otel/user-interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { InstrumentationBase, isWrapped } from '@opentelemetry/instrumentation'

import * as api from '@opentelemetry/api'
import { Attributes } from '@opentelemetry/api'
import { hrTime } from '@opentelemetry/core'
import {
EventName,
Expand All @@ -12,10 +13,15 @@ import {
import { SpanData } from '@opentelemetry/instrumentation-user-interaction/build/src/internal-types'
import { getElementXPath } from '@opentelemetry/sdk-trace-web'
import { AsyncTask } from '@opentelemetry/instrumentation-user-interaction/build/esnext/internal-types'

import { ProductAnalyticsEvents } from 'client/types/observe'
import * as SemanticAttributes from '@opentelemetry/semantic-conventions'
const ZONE_CONTEXT_KEY = 'OT_ZONE_CONTEXT'
const EVENT_NAVIGATION_NAME = 'Navigation:'
const DEFAULT_EVENT_NAMES = ['click', 'input', 'submit'] as const

export type UserInteractionConfig = UserInteractionInstrumentationConfig & {
productAnalyticsEvents?: ProductAnalyticsEvents
getLDContextKeyAttributes?: () => Attributes | undefined
}

function defaultShouldPreventSpanCreation() {
return false
Expand Down Expand Up @@ -43,16 +49,23 @@ export class UserInteractionInstrumentation extends InstrumentationBase {
>()
private _eventNames: Set<EventName>
private _shouldPreventSpanCreation: ShouldPreventSpanCreation
private _getLDContextKeyAttributes:
| (() => Attributes | undefined)
| undefined

constructor(config: UserInteractionInstrumentationConfig = {}) {
constructor(config: UserInteractionConfig = {}) {
super(
UserInteractionInstrumentation.moduleName,
UserInteractionInstrumentation.version,
config,
)
this._eventNames = new Set(
(config?.eventNames ?? DEFAULT_EVENT_NAMES) as EventName[],
)

this._getLDContextKeyAttributes = config.getLDContextKeyAttributes
this._eventNames = new Set((config?.eventNames ?? []) as EventName[])
if (config.productAnalyticsEvents?.clicks !== false) {
this._eventNames.add('click')
}

this._shouldPreventSpanCreation =
typeof config?.shouldPreventSpanCreation === 'function'
? config.shouldPreventSpanCreation
Expand Down Expand Up @@ -120,6 +133,8 @@ export class UserInteractionInstrumentation extends InstrumentationBase {
eventName,
{
attributes: {
[SemanticAttributes.ATTR_URL_FULL]:
window.location.href,
['event.type']: eventName,
['event.tag']: element.tagName,
['event.xpath']: xpath,
Expand Down Expand Up @@ -155,6 +170,11 @@ export class UserInteractionInstrumentation extends InstrumentationBase {
}
}

const contextKeys = this._getLDContextKeyAttributes?.()
if (contextKeys) {
span.setAttributes(contextKeys)
}

if (
this._shouldPreventSpanCreation(eventName, element, span) ===
true
Expand Down
2 changes: 0 additions & 2 deletions sdk/highlight-run/src/client/types/client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { RequestResponsePair } from '../listeners/network-listener/utils/models'
import type { EventName } from '@opentelemetry/instrumentation-user-interaction'

export const ALL_CONSOLE_METHODS = [
'assert',
Expand Down Expand Up @@ -141,7 +140,6 @@ export type NetworkRecordingOptions = {

export type OtelOptions = {
instrumentations?: OtelInstrumentatonOptions
eventNames?: EventName[]
}

export type OtelInstrumentatonOptions = {
Expand Down
31 changes: 25 additions & 6 deletions sdk/highlight-run/src/client/types/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
OtelOptions,
} from './client'
import type { CommonOptions } from './types'
import type { EventName } from '@opentelemetry/instrumentation-user-interaction'

export type ObserveOptions = CommonOptions & {
/**
Expand Down Expand Up @@ -61,10 +60,30 @@ export type ObserveOptions = CommonOptions & {
* OTLP HTTP endpoint for OpenTelemetry tracing.
*/
otlpEndpoint?: string
/**
* User interaction instrumentation event names to record.
* Defaults to 'click', 'input', 'submit' window events.
*/
eventNames?: EventName[]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double checking, this is ok to remove?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Synced with @ccschmitz-launchdarkly and decided that we should remove support for this

}
/**
* Specifies whether to record product analytics events.
* @default true
*/
productAnalytics?: boolean | ProductAnalyticsEvents
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dyozie-ld for any feedback on api

}

export type ProductAnalyticsEvents = {
/**
* Specifies whether to record product analytics for clicks.
* Requires the use of the @opentelemetry/instrumentation-user-interaction instrumentation.
* @default true
*/
clicks?: boolean
/**
* Specifies whether to record product analytics for page views.
* Requires the use of the @opentelemetry/instrumentation-document-load instrumentation.
* @default true
*/
pageViews?: boolean
/**
* Specifies whether to record product analytics for custom events.
* @default true
*/
trackEvents?: boolean
}
6 changes: 5 additions & 1 deletion sdk/highlight-run/src/integrations/launchdarkly/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import type {
} from '@launchdarkly/js-client-sdk'
import { LDEvaluationReason } from '@launchdarkly/js-sdk-common/dist/cjs/api/data/LDEvaluationReason'

export type { Hook, LDClient }
export type { Hook, LDClient, LDContextStrict }

export const FEATURE_FLAG_SCOPE = 'feature_flag'
export const LD_SCOPE = 'launchdarkly'
Expand Down Expand Up @@ -45,8 +45,12 @@ export const FEATURE_FLAG_VARIATION_INDEX_ATTR = `${FEATURE_FLAG_SCOPE}.result.v
export const FEATURE_FLAG_APP_ID_ATTR = `${LD_SCOPE}.application.id`
export const FEATURE_FLAG_APP_VERSION_ATTR = `${LD_SCOPE}.application.version`

export const PRODUCT_ANALYTICS_SCOPE = 'product_analytics'
export const PRODUCT_ANALYTICS_CONTEXT_ATTR = `${PRODUCT_ANALYTICS_SCOPE}.context_keys`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Do we want the context information under the product_analytics scope? At least in a spans attributes.

I imagine the context being a more generic attribute which we can add to spans across various use cases, so I prefer not locking it into PA even though it fits for this instance.

Maybe context.context_keys.<key>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, open to changing this. I wasn't sure how important this was to be unique for processing the traces into the "fact tables", or how overloaded context may be (doesn't look like we are using it currently). May see if @mayberryzane has a preference?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agree that context probably makes more sense than product_analytics since it's not PA specific


export const LD_INITIALIZE_EVENT = '$ld:telemetry:session:init'
export const LD_TRACK_EVENT = '$ld:telemetry:track'
export const LD_TRACK_SPAN_NAME = 'launchdarkly.track'
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How the name will be displayed in traces


export const LD_METRIC_NAME_DOCUMENT_LOAD = 'document_load'

Expand Down
29 changes: 26 additions & 3 deletions sdk/highlight-run/src/plugins/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getContextKeys,
Hook,
LD_IDENTIFY_RESULT_STATUS,
LD_TRACK_SPAN_NAME,
LDClient,
} from '../integrations/launchdarkly'
import { Observe as ObserveAPI } from '../api/observe'
Expand All @@ -26,6 +27,7 @@ import { Plugin } from './common'
import {
ATTR_TELEMETRY_SDK_NAME,
ATTR_TELEMETRY_SDK_VERSION,
ATTR_URL_FULL,
} from '@opentelemetry/semantic-conventions'
import { Attributes } from '@opentelemetry/api'
import { internalLog } from '../sdk/util'
Expand Down Expand Up @@ -126,9 +128,12 @@ export class Observe extends Plugin<ObserveOptions> implements LDPlugin {
hook.afterIdentify?.(hookContext, data, result)
}

const ldContextKeys = getContextKeys(hookContext.context)
this.observe?.setLDContextKeys(ldContextKeys)

if (result.status === 'completed') {
const metadata = {
...getContextKeys(hookContext.context),
...ldContextKeys,
key:
this.options?.contextFriendlyName?.(
hookContext.context,
Expand Down Expand Up @@ -192,14 +197,32 @@ export class Observe extends Plugin<ObserveOptions> implements LDPlugin {
[]) {
hook.afterTrack?.(hookContext)
}
this.observe?.recordLog('LD.track', 'info', {

const trackAttrs: Attributes = {
[ATTR_URL_FULL]: window.location.href,
...(this.observe?.getLDContextKeyAttributes() ?? {}),
...metaAttrs,
key: hookContext.key,
value: hookContext.metricValue,
...(typeof hookContext.data === 'object'
? hookContext.data
: {}),
})
}

const trackEventsEnabled =
this.options?.productAnalytics !== false &&
(typeof this.options?.productAnalytics !== 'object' ||
this.options.productAnalytics.trackEvents !== false)

if (trackEventsEnabled) {
this.observe?.startSpan(LD_TRACK_SPAN_NAME, (s) => {
if (s) {
s.setAttributes(trackAttrs)
}
})
}

this.observe?.recordLog('LD.track', 'info', trackAttrs)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still want a log?

},
},
]
Expand Down
8 changes: 8 additions & 0 deletions sdk/highlight-run/src/sdk/LDObserve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ class _LDObserve extends BufferedClass<Observe> implements Observe {
type,
])
}

setLDContextKeys(contextKeys: Attributes) {
return this._bufferCall('setLDContextKeys', [contextKeys])
}

getLDContextKeyAttributes(): Attributes | undefined {
return this._bufferCall('getLDContextKeyAttributes', [])
}
}

interface GlobalThis {
Expand Down
Loading
Loading