Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky committed Jun 1, 2024
1 parent e802c51 commit adc3a55
Show file tree
Hide file tree
Showing 8 changed files with 94 additions and 65 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { AnyAnalytics } from '../../types'
import { EmitSignal } from '../emitter'

/**
* Helper / facade that wraps the analytics, and abstracts away the details of the analytics instance.
*/
export class AnalyticsService {
private instance: AnyAnalytics
private signalEmitter: EmitSignal
constructor(analyticsInstance: AnyAnalytics, signalEmitter: EmitSignal) {
this.instance = analyticsInstance
this.signalEmitter = signalEmitter
}
get writeKey() {
return this.instance.settings.writeKey
}
addSegmentEventEmitter() {
this.instance.addSourceMiddleware(({ payload, next }) => {
this.signalEmitter.emit({
type: 'instrumentation',
data: payload.obj,
})
next(payload)
})
}
}
17 changes: 10 additions & 7 deletions packages/signals/browser-signals/src/core/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,28 @@ const MAGIC_EVENT_NAME = 'Segment Signal Generated'
* This currently just uses the Segment analytics-next library to send signals.
* This persists the signals in a queue until the client is initialized.
*/
export class SignalsClient {
private queue: {
export class SignalsIngestClient {
private buffer: {
type: SignalType
data: Record<string, any>
}[]
private analytics: Analytics | undefined
constructor() {
this.queue = []
this.buffer = []
this.analytics = undefined
}

initAndFlush(writeKey: string) {
/**
* Initialize analytics and flush the queue.
*/
init(writeKey: string) {
this.analytics = new Analytics({ writeKey })
this.flush()
}

send(type: SignalType, data: Record<string, any>) {
if (!this.analytics) {
this.queue.push({ type, data })
this.buffer.push({ type, data })
} else {
void this.analytics!.track(MAGIC_EVENT_NAME, {
type,
Expand All @@ -38,12 +41,12 @@ export class SignalsClient {
if (!this.analytics) {
throw new Error('Please initialize before calling this method.')
}
this.queue.forEach(({ type, data }) => {
this.buffer.forEach(({ type, data }) => {
void this.analytics!.track(MAGIC_EVENT_NAME, {
type,
data,
})
})
this.queue = []
this.buffer = []
}
}
6 changes: 5 additions & 1 deletion packages/signals/browser-signals/src/core/emitter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Emitter } from '@segment/analytics-generic-utils'
import { logger } from '../../lib/logger'
import { Signal } from '../../types'

export class SignalEmitter {
export interface EmitSignal {
emit: (signal: Signal) => void
}

export class SignalEmitter implements EmitSignal {
private emitter = new Emitter<{ add: [Signal] }>()

emit(signal: Signal) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { SignalType } from '../../types'
import { SignalEmitter } from '../emitter'
import { SignalGenerator } from './types'

Expand All @@ -20,7 +19,7 @@ export class ClickSignalsGenerator implements SignalGenerator {
const handleClick = (ev: MouseEvent) => {
const target = (ev.target as Element) ?? {}
emitter.emit({
type: SignalType.Interaction,
type: 'interaction',
data: {
eventType: 'click',
target: parseElement(target),
Expand All @@ -38,7 +37,7 @@ export class FormSubmitGenerator implements SignalGenerator {
const handleSubmit = (ev: SubmitEvent) => {
const target = ev.submitter!
emitter.emit({
type: SignalType.Interaction,
type: 'interaction',
data: {
eventType: 'submit',
submitter: parseElement(target),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { isClass } from '../../utils/is-class'
import { SignalEmitter } from '../emitter'
import { SignalGeneratorClass, SignalGenerator } from './types'

export const registerGenerator = (
emitter: SignalEmitter,
signalGenerators: (SignalGeneratorClass | SignalGenerator)[]
): VoidFunction => {
const cleanupFns = signalGenerators.map((gen) => {
if (isClass(gen)) {
// Check if Gen is a function and has a constructor
return new gen().register(emitter)
} else {
return gen.register(emitter)
}
})
// Return a cleanup function that calls all the cleanup functions (e.g unsubscribes from event listeners)
return () => cleanupFns.forEach((fn) => fn())
}
70 changes: 27 additions & 43 deletions packages/signals/browser-signals/src/core/signals/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SignalsClient } from '../client'
import { SignalsIngestClient } from '../client'
import { SignalBuffer } from '../buffer'
import { SignalEmitter } from '../emitter'
import {
Expand All @@ -10,62 +10,46 @@ import {
SignalGeneratorClass,
} from '../signal-generators/types'
import { AnyAnalytics } from '../../types'
import { isClass } from '../../utils/is-class'
import { registerGenerator } from '../signal-generators/register'
import { AnalyticsService } from '../analytics-service'

export class Signals {
private buffer: SignalBuffer
private signalsEmitter: SignalEmitter
private cleanup: (() => void)[] = []
private signalsApi: SignalsClient
private signalEmitter: SignalEmitter
private cleanup: VoidFunction[] = []
private signalsClient: SignalsIngestClient

constructor() {
this.signalEmitter = new SignalEmitter()
this.buffer = new SignalBuffer()
this.signalsApi = new SignalsClient()
this.signalsEmitter = new SignalEmitter()
this.signalsEmitter.subscribe(this.buffer.add)
this.signalsEmitter.subscribe((signal) => {
this.signalsApi.send(signal.type, signal.data)
this.signalsClient = new SignalsIngestClient()
this.signalEmitter.subscribe(this.buffer.add)
this.signalEmitter.subscribe((signal) => {
this.signalsClient.send(signal.type, signal.data)
})
this.cleanup.push(registerLocalGenerators(this.signalsEmitter))

// add default signal generators
this.registerGenerator([ClickSignalsGenerator, FormSubmitGenerator])
}

/**
* Does two things:
* - Sends any queued signals to the server.
* - Augments the analytics client to listen to a transform signals.
*/
start(analytics: AnyAnalytics) {
this.signalsApi.initAndFlush(analytics.settings.writeKey)
const analyticsService = new AnalyticsService(analytics, this.signalEmitter)
this.signalsClient.init(analyticsService.writeKey)
}

stop() {
this.cleanup.forEach((fn) => fn())
}
}

type CleanupGenerators = () => void

const registerGenerators = (
emitter: SignalEmitter,
signalGenerators: (SignalGeneratorClass | SignalGenerator)[]
): CleanupGenerators => {
const cleanupFns = signalGenerators.map((gen) => {
if (isClass(gen)) {
// Check if Gen is a function and has a constructor
return new gen().register(emitter)
} else {
return gen.register(emitter)
}
})
// Return a cleanup function that calls all the cleanup functions (e.g unsubscribes from event listeners)
return () => cleanupFns.forEach((fn) => fn())
}

const registerLocalGenerators = (emitter: SignalEmitter): CleanupGenerators => {
return registerGenerators(emitter, [
ClickSignalsGenerator,
FormSubmitGenerator,
])
}

const registerSegmentEventGenerators = (
analytics: AnyAnalytics,
emitter: SignalEmitter
): CleanupGenerators => {
return registerGenerators(emitter, [])
/**
* Emit custom signals.
*/
registerGenerator(generators: (SignalGeneratorClass | SignalGenerator)[]) {
this.cleanup.push(registerGenerator(this.signalEmitter, generators))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface CDNSettings {

export interface SegmentEventStub {
context: {}
[key: string]: unknown
}

export interface SourceMiddlewareParams {
Expand Down
15 changes: 4 additions & 11 deletions packages/signals/browser-signals/src/types/signals.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
export enum SignalType {
Navigation = 'navigation', // page events
Instrumentation = 'instrumentation', // from segment
Interaction = 'interaction', // button clicks, form submissions
}
export type SignalType = 'navigation' | 'interaction' | 'instrumentation'

export interface AppSignal<T extends SignalType, Data> {
type: T
Expand All @@ -29,21 +25,18 @@ type SubmitData = {
submitter: SerializedTarget
}

export type InteractionSignal = AppSignal<
SignalType.Interaction,
InteractionData
>
export type InteractionSignal = AppSignal<'interaction', InteractionData>

interface NavigationData {
[key: string]: unknown
}
export type NavigationSignal = AppSignal<SignalType.Navigation, NavigationData>
export type NavigationSignal = AppSignal<'navigation', NavigationData>

interface InstrumentationData {
[key: string]: unknown
}
export type InstrumentationSignal = AppSignal<
SignalType.Instrumentation,
'instrumentation',
InstrumentationData
>

Expand Down

0 comments on commit adc3a55

Please sign in to comment.