Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky committed May 31, 2024
1 parent e0e94de commit e802c51
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 35 deletions.
11 changes: 10 additions & 1 deletion packages/signals/browser-signals/src/core/buffer/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
export class SignalBuffer {}
import { Emitter } from '@segment/analytics-generic-utils'
import { Signal } from '../../types'

export class SignalBuffer extends Emitter<{ add: [Signal] }> {
signals: Signal[] = []
add(signal: Signal) {
this.emit('add', signal)
this.signals.push(signal)
}
}
49 changes: 49 additions & 0 deletions packages/signals/browser-signals/src/core/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Analytics } from '@segment/analytics-next'
import { SignalType } from '../../types'

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: {
type: SignalType
data: Record<string, any>
}[]
private analytics: Analytics | undefined
constructor() {
this.queue = []
this.analytics = undefined
}

initAndFlush(writeKey: string) {
this.analytics = new Analytics({ writeKey })
this.flush()
}

send(type: SignalType, data: Record<string, any>) {
if (!this.analytics) {
this.queue.push({ type, data })
} else {
void this.analytics!.track(MAGIC_EVENT_NAME, {
type,
data,
})
}
}

flush() {
if (!this.analytics) {
throw new Error('Please initialize before calling this method.')
}
this.queue.forEach(({ type, data }) => {
void this.analytics!.track(MAGIC_EVENT_NAME, {
type,
data,
})
})
this.queue = []
}
}
2 changes: 1 addition & 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,7 @@ import { Emitter } from '@segment/analytics-generic-utils'
import { logger } from '../../lib/logger'
import { Signal } from '../../types'

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

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

const parseElement = (el: Element) => {
Expand All @@ -16,7 +16,7 @@ const parseElement = (el: Element) => {

export class ClickSignalsGenerator implements SignalGenerator {
name = 'click'
register(emitter: SignalsEmitter) {
register(emitter: SignalEmitter) {
const handleClick = (ev: MouseEvent) => {
const target = (ev.target as Element) ?? {}
emitter.emit({
Expand All @@ -34,7 +34,7 @@ export class ClickSignalsGenerator implements SignalGenerator {

export class FormSubmitGenerator implements SignalGenerator {
name = 'form-submit'
register(emitter: SignalsEmitter) {
register(emitter: SignalEmitter) {
const handleSubmit = (ev: SubmitEvent) => {
const target = ev.submitter!
emitter.emit({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import type { SignalsEmitter } from '../emitter'
import type { SignalEmitter } from '../emitter'

export interface SignalGenerator {
/**
* To support unregistering by name/label
* e.g "form-submit"
*/
name: string
register(emitter: SignalsEmitter): () => void
/**
* Register a custom function that emits signals
*/
register(emitter: SignalEmitter): () => void
}

export type SignalGeneratorClass = new () => SignalGenerator
export interface SignalGeneratorClass {
new (): SignalGenerator
}
77 changes: 50 additions & 27 deletions packages/signals/browser-signals/src/core/signals/index.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,71 @@
import { SignalsClient } from '../client'
import { SignalBuffer } from '../buffer'
import { SignalsEmitter } from '../emitter'
import { SignalEmitter } from '../emitter'
import {
ClickSignalsGenerator,
FormSubmitGenerator,
} from '../signal-generators/dom-generators'
import { SignalGeneratorClass } from '../signal-generators/types'
import {
SignalGenerator,
SignalGeneratorClass,
} from '../signal-generators/types'
import { AnyAnalytics } from '../../types'
import { isClass } from '../../utils/is-class'

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

constructor() {
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.cleanup.push(registerLocalGenerators(this.signalsEmitter))
}

start(analytics: AnyAnalytics) {
this.signalsApi.initAndFlush(analytics.settings.writeKey)
}

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

type CleanupGenerators = () => void

const registerGenerators = (
emitter: SignalsEmitter,
signalGenerators: SignalGeneratorClass[]
emitter: SignalEmitter,
signalGenerators: (SignalGeneratorClass | SignalGenerator)[]
): CleanupGenerators => {
const cleanupFns = signalGenerators.map((Gen) => new Gen().register(emitter))
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: SignalsEmitter
): CleanupGenerators => {
const registerLocalGenerators = (emitter: SignalEmitter): CleanupGenerators => {
return registerGenerators(emitter, [
ClickSignalsGenerator,
FormSubmitGenerator,
])
}

export class Signals {
private buffer = new SignalBuffer()
private signalsEmitter: SignalsEmitter
private cleanupGenerators: CleanupGenerators

constructor() {
this.signalsEmitter = new SignalsEmitter()
this.cleanupGenerators = registerLocalGenerators(this.signalsEmitter)
}

start(analytics: any) {
this.signalsEmitter.subscribe((signal) => {
this.buffer.add(signal)
})
}

stop() {
this.cleanupGenerators()
}
const registerSegmentEventGenerators = (
analytics: AnyAnalytics,
emitter: SignalEmitter
): CleanupGenerators => {
return registerGenerators(emitter, [])
}
4 changes: 4 additions & 0 deletions packages/signals/browser-signals/src/types/analytics-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface DestinationMiddlewareParams {
}

export interface AnyAnalytics {
settings: {
cdnSettings: CDNSettings
writeKey: string
}
addSourceMiddleware(
middleware: Function | SourceMiddlewareFunction
): any | this
Expand Down
5 changes: 5 additions & 0 deletions packages/signals/browser-signals/src/utils/is-class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const isClass = (value: any): value is NewableFunction => {
return (
typeof value === 'function' && value.prototype.constructor !== undefined
)
}

0 comments on commit e802c51

Please sign in to comment.