Skip to content
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

Filter out repeated seek and volume change events happening when user holds down the player controls (close #1218) #1219

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@snowplow/browser-plugin-media",
"comment": "Filter out repeated seek and volume change events happening when user holds down the player controls (#1218)",
"type": "none"
}
],
"packageName": "@snowplow/browser-plugin-media"
}
25 changes: 11 additions & 14 deletions plugins/browser-plugin-media/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
MediaTrackQualityChangeArguments,
MediaTrackErrorArguments,
MediaTrackSelfDescribingEventArguments,
EventWithContext,
} from './types';

export { MediaAdBreakType as MediaPlayerAdBreakType };
Expand Down Expand Up @@ -78,20 +79,21 @@ export function startMediaTracking(
config.boundaries,
config.captureEvents,
config.updatePageActivityWhilePlaying,
config.filterOutRepeatedEvents,
config.context
);
activeMedias[mediaTracking.id] = mediaTracking;
}

/**
* Ends media tracking with the given ID if previously started.
* Clears local state for the media tracking.
* Clears local state for the media tracking and sends any events waiting to be sent.
*
* @param configuration Configuration with the media tracking ID
*/
export function endMediaTracking(configuration: { id: string }) {
if (activeMedias[configuration.id]) {
activeMedias[configuration.id].stop();
activeMedias[configuration.id].flushAndStop();
delete activeMedias[configuration.id];
}
}
Expand Down Expand Up @@ -704,23 +706,18 @@ function track(
return;
}

const events = mediaTracking.update(mediaEvent, customEvent, player, ad, adBreak);
const trackEvent = (event: EventWithContext) => {
dispatchToTrackersInCollection(trackers, _trackers, (t) => {
t.core.track(buildSelfDescribingEvent(event), (event.context ?? []).concat(context), timestamp);
});
};

mediaTracking.update(trackEvent, mediaEvent, customEvent, player, ad, adBreak);

// Update page activity in order to keep sending page pings if needed
if (mediaTracking.shouldUpdatePageActivity()) {
dispatchToTrackersInCollection(trackers, _trackers, (t) => {
t.updatePageActivity();
});
}

if (events.length == 0) {
return;
}

// Send all created events to the trackers
dispatchToTrackersInCollection(trackers, _trackers, (t) => {
events.forEach((event) => {
t.core.track(buildSelfDescribingEvent(event), event.context.concat(context), timestamp);
});
});
}
43 changes: 14 additions & 29 deletions plugins/browser-plugin-media/src/mediaTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import {
MediaPlayerUpdate,
MediaEventType,
MediaEvent,
EventWithContext,
FilterOutRepeatedEvents,
} from './types';
import { RepeatedEventFilter } from './repeatedEventFilter';

/**
* Manages the state and built-in entities for a media tracking that starts when a user
Expand Down Expand Up @@ -39,14 +42,14 @@ export class MediaTracking {
private pingInterval?: MediaPingInterval;
/// Manages ad entities.
private adTracking = new MediaAdTracking();
/// Used to prevent tracking seek start events multiple times.
private isSeeking = false;
/// Context entities to attach to all events
private customContext?: Array<SelfDescribingJson>;
/// Optional list of event types to allow tracking and discard others.
private captureEvents?: MediaEventType[];
// Whether to update page activity when playing media. Enabled by default.
private updatePageActivityWhilePlaying?: boolean;
/// Filters out repeated events based on the configuration.
private repeatedEventFilter: RepeatedEventFilter;

constructor(
id: string,
Expand All @@ -56,6 +59,7 @@ export class MediaTracking {
boundaries?: number[],
captureEvents?: MediaEventType[],
updatePageActivityWhilePlaying?: boolean,
filterRepeatedEvents?: FilterOutRepeatedEvents,
context?: Array<SelfDescribingJson>
) {
this.id = id;
Expand All @@ -66,6 +70,7 @@ export class MediaTracking {
this.captureEvents = captureEvents;
this.updatePageActivityWhilePlaying = updatePageActivityWhilePlaying;
this.customContext = context;
this.repeatedEventFilter = new RepeatedEventFilter(filterRepeatedEvents);

// validate event names in the captureEvents list
captureEvents?.forEach((eventType) => {
Expand All @@ -78,8 +83,9 @@ export class MediaTracking {
/**
* Called when user calls `endMediaTracking()`.
*/
stop() {
flushAndStop() {
this.pingInterval?.clear();
this.repeatedEventFilter.flush();
}

/**
Expand All @@ -91,12 +97,13 @@ export class MediaTracking {
* @returns List of events with entities to track.
*/
update(
trackEvent: (event: EventWithContext) => void,
mediaEvent?: MediaEvent,
customEvent?: SelfDescribingJson,
player?: MediaPlayerUpdate,
ad?: MediaAdUpdate,
adBreak?: MediaPlayerAdBreakUpdate
): { event: SelfDescribingJson; context: SelfDescribingJson[] }[] {
) {
// update state
this.updatePlayer(player);
if (mediaEvent !== undefined) {
Expand Down Expand Up @@ -138,7 +145,8 @@ export class MediaTracking {
if (customEvent !== undefined) {
eventsToTrack.push({ event: customEvent, context: context });
}
return eventsToTrack;

this.repeatedEventFilter.trackFilteredEvents(eventsToTrack, trackEvent);
}

shouldUpdatePageActivity(): boolean {
Expand Down Expand Up @@ -173,34 +181,11 @@ export class MediaTracking {
}

private shouldTrackEvent(eventType: MediaEventType): boolean {
return this.updateSeekingAndCheckIfShouldTrack(eventType) && this.allowedToCaptureEventType(eventType);
}

/** Prevents multiple seek start events to be tracked after each other without a seek end (happens when scrubbing). */
private updateSeekingAndCheckIfShouldTrack(eventType: MediaEventType): boolean {
if (eventType == MediaEventType.SeekStart) {
if (this.isSeeking) {
return false;
}

this.isSeeking = true;
} else if (eventType == MediaEventType.SeekEnd) {
this.isSeeking = false;
}

return true;
}

private allowedToCaptureEventType(eventType: MediaEventType): boolean {
return this.captureEvents === undefined || this.captureEvents.includes(eventType);
}

private getPercentProgress(): number | undefined {
if (
this.player.duration === null ||
this.player.duration === undefined ||
this.player.duration == 0
) {
if (this.player.duration === null || this.player.duration === undefined || this.player.duration == 0) {
return undefined;
}
return Math.floor(((this.player.currentTime ?? 0) / this.player.duration) * 100);
Expand Down
79 changes: 79 additions & 0 deletions plugins/browser-plugin-media/src/repeatedEventFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { getMediaEventSchema } from './schemata';
import { FilterOutRepeatedEvents, MediaEventType, EventWithContext } from './types';

/**
* This class filters out repeated events that are sent by the media player.
* This applies to seek and volume change events.
*/
export class RepeatedEventFilter {
private aggregateEventsWithOrder: { [schema: string]: boolean } = {};
private eventsToAggregate: { [schema: string]: (() => void)[] } = {};
private flushTimeout?: number;
private flushTimeoutMs: number;

constructor(configuration?: FilterOutRepeatedEvents) {
let allFiltersEnabled = configuration === undefined || configuration === true;
if (allFiltersEnabled || (typeof configuration === 'object' && configuration.seekEvents !== false)) {
this.aggregateEventsWithOrder[getMediaEventSchema(MediaEventType.SeekStart)] = true;
this.aggregateEventsWithOrder[getMediaEventSchema(MediaEventType.SeekEnd)] = false;
}
if (allFiltersEnabled || (typeof configuration === 'object' && configuration.volumeChangeEvents !== false)) {
this.aggregateEventsWithOrder[getMediaEventSchema(MediaEventType.VolumeChange)] = false;
}

this.flushTimeoutMs = (typeof configuration === 'object' ? configuration.flushTimeoutMs : undefined) ?? 5000;

Object.keys(this.aggregateEventsWithOrder).forEach((schema) => {
this.eventsToAggregate[schema] = [];
});
}

trackFilteredEvents(events: EventWithContext[], trackEvent: (event: EventWithContext) => void) {
let startFlushTimeout = false;

events.forEach(({ event, context }) => {
if (this.eventsToAggregate[event.schema] !== undefined) {
startFlushTimeout = true;
this.eventsToAggregate[event.schema].push(() => trackEvent({ event, context }));
} else {
startFlushTimeout = false;
// flush any events waiting
this.flush();

trackEvent({ event, context });
}
});

if (startFlushTimeout && this.flushTimeout === undefined) {
this.setFlushTimeout();
}
}

flush() {
this.clearFlushTimeout();

Object.keys(this.eventsToAggregate).forEach((schema) => {
greg-el marked this conversation as resolved.
Show resolved Hide resolved
let eventsToAggregate = this.eventsToAggregate[schema];
if (eventsToAggregate.length > 0) {
if (this.aggregateEventsWithOrder[schema]) {
eventsToAggregate[0]();
} else {
eventsToAggregate[eventsToAggregate.length - 1]();
}
this.eventsToAggregate[schema] = [];
}
});
}

private clearFlushTimeout() {
if (this.flushTimeout !== undefined) {
clearTimeout(this.flushTimeout);
this.flushTimeout = undefined;
}
}

private setFlushTimeout() {
this.clearFlushTimeout();
this.flushTimeout = window.setTimeout(() => this.flush(), this.flushTimeoutMs);
}
}
43 changes: 39 additions & 4 deletions plugins/browser-plugin-media/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,40 @@ export enum MediaEventType {
Error = 'error',
}

/**
* Configuration for filtering out repeated events of the same type tracked after each other.
* By default, the seek start, seek end and volume change events are filtered out.
*/
export type FilterOutRepeatedEvents =
| {
/**
* Whether to filter out seek start and end events tracked after each other.
*/
seekEvents?: boolean;
/**
* Whether to filter out volume change events tracked after each other.
*/
volumeChangeEvents?: boolean;
/**
* Timeout in milliseconds after which to send the events that are queued for filtering.
* Defaults to 5000 ms.
*/
flushTimeoutMs?: number;
}
| boolean;

export type MediaTrackingConfiguration = {
/** Unique ID of the media tracking. The same ID will be used for media player session if enabled. */
id: string;
/** Attributes for the media player context entity */
player?: MediaPlayerUpdate;
/** Attributes for the media player session context entity or false to disable it. Enabled by default. */
session?: {
/** Local date-time timestamp of when the session started. Automatically set to current time if not given. */
startedAt?: Date
} | false;
session?:
| {
/** Local date-time timestamp of when the session started. Automatically set to current time if not given. */
startedAt?: Date;
}
| false;
/** Configuration for sending ping events. Enabled by default. */
pings?:
| {
Expand All @@ -101,6 +125,13 @@ export type MediaTrackingConfiguration = {
* Otherwise, tracked event types not present in the list will be discarded.
*/
captureEvents?: MediaEventType[];
/**
* Whether to filter out repeated events of the same type tracked after each other.
* Useful to filter out repeated seek and volume change events tracked when the user holds down the seek or volume control.
* Only applies to seek and volume change events.
* Defaults to true.
*/
filterOutRepeatedEvents?: FilterOutRepeatedEvents;
};

export type MediaTrackPlaybackRateChangeArguments = {
Expand Down Expand Up @@ -380,3 +411,7 @@ export interface CommonMediaEventProperties extends CommonEventProperties {
/** Add context entities to an event by setting an Array of Self Describing JSON */
context?: Array<SelfDescribingJson>;
}

export interface EventWithContext extends CommonEventProperties {
event: SelfDescribingJson;
}
Loading
Loading