Skip to content

Commit

Permalink
add signals
Browse files Browse the repository at this point in the history
  • Loading branch information
silesky committed Jun 11, 2024
1 parent 7a19b38 commit ac66ae3
Show file tree
Hide file tree
Showing 41 changed files with 1,812 additions and 140 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,4 @@ reports/*
playwright-report/
playwright/.cache/
tmp.tsconfig.json
.env
6 changes: 6 additions & 0 deletions packages/browser/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ export type { MiddlewareFunction } from './plugins/middleware'
export { getGlobalAnalytics } from './lib/global-analytics-helper'
export { UniversalStorage, Store, StorageObject } from './core/storage'
export { segmentio } from './plugins/segmentio'
export {
resolveAliasArguments,
resolveArguments,
resolvePageArguments,
resolveUserArguments,
} from './core/arguments-resolver'

This file was deleted.

1 change: 1 addition & 0 deletions packages/signals/browser-signals/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ const { createJestTSConfig } = require('@internal/config')

module.exports = createJestTSConfig(__dirname, {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['./jest.setup.js'],
})
3 changes: 3 additions & 0 deletions packages/signals/browser-signals/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* eslint-disable no-undef */
require('fake-indexeddb/auto')
globalThis.structuredClone = (v) => JSON.parse(JSON.stringify(v))
8 changes: 6 additions & 2 deletions packages/signals/browser-signals/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
"webpack": "yarn run -T webpack"
},
"dependencies": {
"tslib": "^2.4.1"
"@segment/analytics-generic-utils": "1.2.0",
"idb": "^8.0.0",
"tslib": "^2.4.1",
"workerboxjs": "^6.1.1"
},
"peerDependencies": {
"@segment/analytics-next": ">=1.70.0"
Expand All @@ -50,6 +53,7 @@
"packageManager": "yarn@3.4.1",
"devDependencies": {
"@internal/config-webpack": "workspace:^",
"@internal/test-helpers": "workspace:^"
"@internal/test-helpers": "workspace:^",
"fake-indexeddb": "^6.0.0"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AnyAnalytics } from '../../types'
import { SignalGenerator } from '../signal-generators/types'

/**
* Helper / facade that wraps the analytics, and abstracts away the details of the analytics instance.
*/
export class AnalyticsService {
private instance: AnyAnalytics
constructor(analyticsInstance: AnyAnalytics) {
this.instance = analyticsInstance
}
get writeKey() {
return this.instance.settings.writeKey
}
createSegmentInstrumentationEventGenerator(): SignalGenerator {
let disable = false
const generator: SignalGenerator = {
id: 'segment-event-generator',
register: async (signalEmitter) => {
await this.instance.addSourceMiddleware(({ payload, next }) => {
if (disable) {
return
}
const event = payload.obj

const isEventFromSignalEdgeFunction =
event.context.__eventOrigin?.type === 'Signal'

if (isEventFromSignalEdgeFunction) {
return next(payload)
} else {
signalEmitter.emit({
type: 'instrumentation',
data: event,
})
}
})
return () => {
disable = true
}
},
}
return generator
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import { SignalBuffer } from '../index'
import { createMockSignal } from '../../../test-helpers/fixtures/signal'
import { getSignalBuffer, SignalBuffer } from '../index'

describe(SignalBuffer, () => {
describe(getSignalBuffer, () => {
let buffer: SignalBuffer

beforeEach(() => {
buffer = new SignalBuffer()
beforeEach(async () => {
buffer = getSignalBuffer({
maxBufferSize: 10,
})
await buffer.clear()
})

it('should exits', () => {
it('should instantiate without throwing an error', () => {
expect(buffer).toBeTruthy()
})
it('should add and clear', async () => {
const mockSignal = createMockSignal()
await buffer.add(mockSignal)
await expect(buffer.getAll()).resolves.toEqual([mockSignal])
await buffer.clear()
await expect(buffer.getAll()).resolves.toHaveLength(0)
})
})
135 changes: 134 additions & 1 deletion packages/signals/browser-signals/src/core/buffer/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,134 @@
export class SignalBuffer {}
import { Signal } from '../../types'
import { openDB, DBSchema, IDBPDatabase } from 'idb'
import { logger } from '../../lib/logger'

interface SignalDatabase extends DBSchema {
signals: {
key: string
value: Signal
}
}

export interface PersistentStorage<T = any> {
getAll(): Promise<T[]> | T[]
add(value: T): Promise<void> | void
clear(): void
}

export interface SignalPersistentStorage extends PersistentStorage<Signal> {}

export class SignalStore implements SignalPersistentStorage {
static readonly DB_NAME = 'Segment Signals Buffer'
static readonly STORE_NAME = 'signals'
private signalStore: Promise<IDBPDatabase<SignalDatabase>>
private signalCount = 0
private maxBufferSize: number

public length() {
return this.signalCount
}

static deleteDatabase() {
return indexedDB.deleteDatabase(SignalStore.DB_NAME)
}

constructor(settings: { maxBufferSize?: number } = {}) {
this.maxBufferSize = settings.maxBufferSize ?? 25 // TODO: increase this number after development
this.signalStore = this.createSignalStore()
void this.initializeSignalCount()
}

private getStore() {
return this.signalStore
}

private async createSignalStore() {
const db = await openDB<SignalDatabase>(SignalStore.DB_NAME, 1, {
upgrade(db) {
db.createObjectStore(SignalStore.STORE_NAME, { autoIncrement: true })
},
})
logger.debug('Signals Buffer (indexDB) initialized')
return db
}

private async initializeSignalCount() {
const store = await this.signalStore
this.signalCount = await store.count(SignalStore.STORE_NAME)
logger.debug(
`Signal count initialized with ${this.signalCount} signals (max: ${this.maxBufferSize})`
)
}

async add(signal: Signal): Promise<void> {
const store = await this.signalStore
if (this.signalCount >= this.maxBufferSize) {
// Get the key of the oldest signal and delete it
const oldestKey = await store
.transaction(SignalStore.STORE_NAME)
.store.getKey(IDBKeyRange.lowerBound(0))
if (oldestKey !== undefined) {
await store.delete(SignalStore.STORE_NAME, oldestKey)
} else {
this.signalCount--
}
}
await store.add(SignalStore.STORE_NAME, signal)
this.signalCount++
}

async getAll(): Promise<Signal[]> {
const store = await this.getStore()
return store.getAll(SignalStore.STORE_NAME)
}

async clear() {
const store = await this.getStore()
return store.clear(SignalStore.STORE_NAME)
}
}

export class SignalBuffer<
T extends SignalPersistentStorage = SignalPersistentStorage
> {
public store: T
constructor(store: T) {
this.store = store
}
async add(signal: Signal) {
try {
return await this.store.add(signal)
} catch (e) {
console.error(e)
return undefined
}
}
async getAll() {
try {
return await this.store.getAll()
} catch (e) {
console.error(e)
return []
}
}
async clear() {
try {
return await this.store.clear()
} catch (e) {
console.error(e)
return undefined
}
}
}

export const getSignalBuffer = <
T extends SignalPersistentStorage = SignalPersistentStorage
>(
settings: {
maxBufferSize?: number
signalStorage?: T
} = {}
) => {
const store = settings.signalStorage ?? new SignalStore(settings)
return new SignalBuffer(store)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { SignalsIngestClient } from '../index'
import { createSuccess } from '@segment/analytics-next/src/test-helpers/factories'
import unfetch from 'unfetch'
jest.mock('unfetch')
jest
.mocked(unfetch)
.mockImplementation(() => createSuccess({ integrations: {} }))

describe(SignalsIngestClient, () => {
let client: SignalsIngestClient

beforeEach(async () => {
client = new SignalsIngestClient()
await client.init({ writeKey: 'test' })
})

it('makes a track call via the analytics api', async () => {
expect(client).toBeTruthy()
const ctx = await client.send({
type: 'instrumentation',
data: {
foo: 'bar',
},
})

expect(ctx!.event.type).toEqual('track')
expect(ctx!.event.properties).toEqual({
type: 'instrumentation',
data: {
foo: 'bar',
},
})
})
})
Loading

0 comments on commit ac66ae3

Please sign in to comment.