-
Notifications
You must be signed in to change notification settings - Fork 49
feat(autocapture): fetch page actions from remote config #1168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ac96bca
0b7deb8
bb42e66
863c47d
d10e0cd
b3b2771
249dd07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import { | |
DEFAULT_ACTION_CLICK_ALLOWLIST, | ||
DEFAULT_DATA_ATTRIBUTE_PREFIX, | ||
} from '@amplitude/analytics-core'; | ||
import { createRemoteConfigFetch } from '@amplitude/analytics-remote-config'; | ||
import * as constants from './constants'; | ||
import { fromEvent, map, Observable, Subscription, share } from 'rxjs'; | ||
import { | ||
|
@@ -28,6 +29,7 @@ import { createMutationObservable, createClickObservable } from './observables'; | |
|
||
import { | ||
createLabeledEventToTriggerMap, | ||
generateEvaluateTriggers, | ||
groupLabeledEventIdsByEventType, | ||
matchEventToLabeledEvents, | ||
matchLabeledEventsToTriggers, | ||
|
@@ -152,14 +154,14 @@ export const autocapturePlugin = (options: ElementInteractionsOptions = {}): Bro | |
}; | ||
|
||
// Group labeled events by event type (eg. click, change) | ||
const groupedLabeledEvents = groupLabeledEventIdsByEventType(Object.values(options.pageActions?.labeledEvents ?? {})); | ||
let groupedLabeledEvents = groupLabeledEventIdsByEventType(Object.values(options.pageActions?.labeledEvents ?? {})); | ||
|
||
const labeledEventToTriggerMap = createLabeledEventToTriggerMap(options.pageActions?.triggers ?? []); | ||
let labeledEventToTriggerMap = createLabeledEventToTriggerMap(options.pageActions?.triggers ?? []); | ||
|
||
// Evaluate triggers for the given event by running the actions associated with the matching triggers | ||
const evaluateTriggers = <T extends ElementBasedEvent>( | ||
event: ElementBasedTimestampedEvent<T>, | ||
): ElementBasedTimestampedEvent<T> => { | ||
let evaluateTriggers = ( | ||
event: ElementBasedTimestampedEvent<ElementBasedEvent>, | ||
): ElementBasedTimestampedEvent<ElementBasedEvent> => { | ||
// If there is no pageActions, return the event as is | ||
const { pageActions } = options; | ||
if (!pageActions) { | ||
|
@@ -180,12 +182,53 @@ export const autocapturePlugin = (options: ElementInteractionsOptions = {}): Bro | |
return event; | ||
}; | ||
|
||
// Function to recalculate internal variables when remote config is updated | ||
const recomputePageActionsData = (remotePageActions: ElementInteractionsOptions['pageActions']) => { | ||
if (remotePageActions) { | ||
// Merge remote config with local options | ||
options.pageActions = { | ||
...options.pageActions, | ||
...remotePageActions, | ||
}; | ||
|
||
// Recalculate internal variables | ||
groupedLabeledEvents = groupLabeledEventIdsByEventType(Object.values(options.pageActions.labeledEvents ?? {})); | ||
labeledEventToTriggerMap = createLabeledEventToTriggerMap(options.pageActions.triggers ?? []); | ||
|
||
// Update evaluateTriggers function | ||
evaluateTriggers = generateEvaluateTriggers(groupedLabeledEvents, labeledEventToTriggerMap, options); | ||
} | ||
}; | ||
|
||
const setup: BrowserEnrichmentPlugin['setup'] = async (config, amplitude) => { | ||
/* istanbul ignore if */ | ||
if (typeof document === 'undefined') { | ||
return; | ||
} | ||
|
||
// Fetch remote config for pageActions in a non-blocking manner | ||
if (config.fetchRemoteConfig) { | ||
createRemoteConfigFetch({ | ||
localConfig: config, | ||
configKeys: ['analyticsSDK.pageActions'], | ||
}) | ||
.then(async (remoteConfigFetch) => { | ||
try { | ||
const remotePageActions = await remoteConfigFetch.getRemoteConfig('analyticsSDK', 'pageActions'); | ||
recomputePageActionsData(remotePageActions as ElementInteractionsOptions['pageActions']); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to confirm, this means that:
|
||
} catch (error) { | ||
// Log error but don't fail the setup | ||
/* istanbul ignore next */ | ||
config?.loggerProvider?.error(`Failed to fetch remote config: ${String(error)}`); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nitpick: this try -> catch block isn't necessary. If the error gets thrown from inside of 'then' it will be caught by 'catch'. |
||
}) | ||
.catch((error) => { | ||
// Log error but don't fail the setup | ||
/* istanbul ignore next */ | ||
config?.loggerProvider?.error(`Failed to create remote config fetch: ${String(error)}`); | ||
}); | ||
} | ||
|
||
// Create should track event functions the different allowlists | ||
const shouldTrackEvent = createShouldTrackEvent( | ||
options, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,39 @@ | ||
import { Trigger } from '@amplitude/analytics-core/lib/esm/types/element-interactions'; | ||
// Return which labeled events, if any, the element matches | ||
import type { LabeledEvent } from '@amplitude/analytics-core/lib/esm/types/element-interactions'; | ||
import type { | ||
ElementInteractionsOptions, | ||
LabeledEvent, | ||
} from '@amplitude/analytics-core/lib/esm/types/element-interactions'; | ||
import { ElementBasedTimestampedEvent, ElementBasedEvent } from 'src/helpers'; | ||
import { matchEventToFilter } from './matchEventToFilter'; | ||
import { executeActions } from './actions'; | ||
|
||
const eventTypeToBrowserEventMap = { | ||
'[Amplitude] Element Clicked': 'click', | ||
'[Amplitude] Element Changed': 'change', | ||
} as const; | ||
// groups labeled events by event type | ||
// skips any labeled events with malformed definitions or unexpected event_type | ||
export const groupLabeledEventIdsByEventType = (labeledEvents?: LabeledEvent[] | null) => { | ||
const groupedLabeledEvents = { | ||
click: new Set<string>(), | ||
change: new Set<string>(), | ||
}; | ||
export const groupLabeledEventIdsByEventType = (labeledEvents: LabeledEvent[] | null | undefined) => { | ||
// Initialize all event types with empty sets | ||
const groupedLabeledEvents = Object.values(eventTypeToBrowserEventMap).reduce((acc, browserEvent) => { | ||
acc[browserEvent] = new Set<string>(); | ||
return acc; | ||
}, {} as Record<string, Set<string>>); | ||
|
||
// If there are no labeled events, return the initialized groupedLabeledEvents | ||
if (!labeledEvents) { | ||
return groupedLabeledEvents; | ||
} | ||
|
||
// Group labeled events by event type | ||
for (const le of labeledEvents) { | ||
try { | ||
for (const def of le.definition) { | ||
groupedLabeledEvents[def.event_type]?.add(le.id); | ||
const browserEvent = eventTypeToBrowserEventMap[def.event_type]; | ||
if (browserEvent) { | ||
groupedLabeledEvents[browserEvent].add(le.id); | ||
} | ||
} | ||
} catch (e) { | ||
// Skip this labeled event if there is an error | ||
|
@@ -64,7 +79,10 @@ export const matchEventToLabeledEvents = ( | |
) => { | ||
return labeledEvents.filter((le) => { | ||
return le.definition.some((def) => { | ||
return def.event_type === event.type && def.filters.every((filter) => matchEventToFilter(event, filter)); | ||
return ( | ||
eventTypeToBrowserEventMap[def.event_type] === event.type && | ||
def.filters.every((filter) => matchEventToFilter(event, filter)) | ||
); | ||
}); | ||
}); | ||
}; | ||
|
@@ -74,8 +92,37 @@ export const matchLabeledEventsToTriggers = (labeledEvents: LabeledEvent[], leTo | |
for (const le of labeledEvents) { | ||
const triggers = leToTriggerMap.get(le.id); | ||
if (triggers) { | ||
triggers.forEach((trigger) => matchingTriggers.add(trigger)); | ||
for (const trigger of triggers) { | ||
matchingTriggers.add(trigger); | ||
} | ||
} | ||
} | ||
return Array.from(matchingTriggers); | ||
}; | ||
|
||
export const generateEvaluateTriggers = ( | ||
groupedLabeledEvents: ReturnType<typeof groupLabeledEventIdsByEventType>, | ||
labeledEventToTriggerMap: ReturnType<typeof createLabeledEventToTriggerMap>, | ||
options: ElementInteractionsOptions, | ||
) => { | ||
return (event: ElementBasedTimestampedEvent<ElementBasedEvent>) => { | ||
// If there is no pageActions, return the event as is | ||
const { pageActions } = options; | ||
if (!pageActions) { | ||
return event; | ||
} | ||
|
||
// Find matching labeled events | ||
const matchingLabeledEvents = matchEventToLabeledEvents( | ||
event, | ||
Array.from(groupedLabeledEvents[event.type]).map((id) => pageActions.labeledEvents[id]), | ||
); | ||
// Find matching conditions | ||
const matchingTriggers = matchLabeledEventsToTriggers(matchingLabeledEvents, labeledEventToTriggerMap); | ||
for (const trigger of matchingTriggers) { | ||
executeActions(trigger.actions, event); | ||
} | ||
|
||
return event; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this function be moved into a helper function? It looks like it does the same thing as |
||
}; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does the remote config endpoint allow us to return
analyticsSDK: { browserSDK, pageActions}
instead of calling these 2 separately?