Skip to content

Commit

Permalink
Signals sampling (#1166)
Browse files Browse the repository at this point in the history
  • Loading branch information
danieljackins authored Oct 29, 2024
1 parent 3b9a9ee commit 9e6db28
Show file tree
Hide file tree
Showing 13 changed files with 136 additions and 31 deletions.
6 changes: 6 additions & 0 deletions .changeset/nasty-cherries-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@segment/analytics-next': minor
'@segment/analytics-signals': minor
---

Add sampling logic and block non debug traffic
7 changes: 7 additions & 0 deletions packages/browser/src/browser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ export interface CDNSettings {
version: number
}
| {}

/**
* Settings for auto instrumentation
*/
autoInstrumentationSettings?: {
sampleRate: number
}
}

export interface AnalyticsBrowserSettings {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class BasePage {
({ signalSettings }) => {
window.signalsPlugin = new window.SignalsPlugin({
disableSignalsRedaction: true,
enableSignalsIngestion: true,
...signalSettings,
})
window.analytics.load({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ test('Collecting signals whenever a user enters text input', async ({
*/
await indexPage.loadAndWait(page, basicEdgeFn, {
disableSignalsRedaction: true,
enableSignalsIngestion: true,
})

await Promise.all([
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test, expect } from '@playwright/test'
import { IndexPage } from './index-page'

const indexPage = new IndexPage()

const basicEdgeFn = `const processSignal = (signal) => {}`

test('ingestion not enabled -> will not send the signal', async ({ page }) => {
await indexPage.loadAndWait(page, basicEdgeFn, {
enableSignalsIngestion: false,
})

await indexPage.fillNameInput('John Doe')
await indexPage.waitForSignalsApiFlush().catch(() => {
expect(true).toBe(true)
})
})

test('ingestion enabled -> will send the signal', async ({ page }) => {
await indexPage.loadAndWait(page, basicEdgeFn, {
enableSignalsIngestion: true,
})

await Promise.all([
indexPage.fillNameInput('John Doe'),
indexPage.waitForSignalsApiFlush(),
])

expect(true).toBe(true)
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ test('redaction enabled -> will XXX the value of text input', async ({
}) => {
await indexPage.loadAndWait(page, basicEdgeFn, {
disableSignalsRedaction: false,
enableSignalsIngestion: true,
})

await Promise.all([
Expand Down Expand Up @@ -40,6 +41,7 @@ test('redation disabled -> will not touch the value of text input', async ({
}) => {
await indexPage.loadAndWait(page, basicEdgeFn, {
disableSignalsRedaction: true,
enableSignalsIngestion: true,
})

await Promise.all([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ describe(SignalsIngestClient, () => {
let client: SignalsIngestClient

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

Expand Down
16 changes: 11 additions & 5 deletions packages/signals/signals/src/core/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,25 @@ export class SignalsIngestSettings {
flushAt: number
flushInterval: number
apiHost: string
shouldDisableSignalRedaction: () => boolean
shouldDisableSignalsRedaction: () => boolean
shouldIngestSignals: () => boolean
writeKey?: string
constructor(settings: SignalsIngestSettingsConfig) {
this.flushAt = settings.flushAt ?? 5
this.apiHost = settings.apiHost ?? 'signals.segment.io/v1'
this.flushInterval = settings.flushInterval ?? 2000
this.shouldDisableSignalRedaction =
settings.shouldDisableSignalRedaction ?? (() => false)
this.shouldDisableSignalsRedaction =
settings.shouldDisableSignalsRedaction ?? (() => false)
this.shouldIngestSignals = settings.shouldIngestSignals ?? (() => false)
}
}

export interface SignalsIngestSettingsConfig {
apiHost?: string
flushAt?: number
flushInterval?: number
shouldDisableSignalRedaction?: () => boolean
shouldDisableSignalsRedaction?: () => boolean
shouldIngestSignals?: () => boolean
}
/**
* This currently just uses the Segment analytics-next library to send signals.
Expand Down Expand Up @@ -73,7 +76,10 @@ export class SignalsIngestClient {
if (!this.analytics) {
throw new Error('Please initialize before calling this method.')
}
const disableRedaction = this.settings.shouldDisableSignalRedaction()
if (!this.settings.shouldIngestSignals()) {
return
}
const disableRedaction = this.settings.shouldDisableSignalsRedaction()
const cleanSignal = disableRedaction ? signal : redactSignalData(signal)

if (disableRedaction) {
Expand Down
86 changes: 61 additions & 25 deletions packages/signals/signals/src/core/signals/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export type SignalsSettingsConfig = Pick<
| 'flushAt'
| 'flushInterval'
| 'disableSignalsRedaction'
| 'enableSignalsIngestion'
| 'networkSignalsAllowList'
| 'networkSignalsDisallowList'
| 'networkSignalsAllowSameDomain'
Expand All @@ -33,7 +34,8 @@ export class SignalGlobalSettings {
ingestClient: SignalsIngestSettingsConfig
network: NetworkSettingsConfig

private redaction = new SignalRedactionSettings()
private sampleSuccess = false
private signalsDebug = new SignalsDebugSettings()

constructor(settings: SignalsSettingsConfig) {
if (settings.maxBufferSize && settings.signalStorage) {
Expand All @@ -42,8 +44,9 @@ export class SignalGlobalSettings {
)
}

this.redaction = new SignalRedactionSettings(
settings.disableSignalsRedaction
this.signalsDebug = new SignalsDebugSettings(
settings.disableSignalsRedaction,
settings.enableSignalsIngestion
)

this.signalBuffer = {
Expand All @@ -54,7 +57,17 @@ export class SignalGlobalSettings {
apiHost: settings.apiHost,
flushAt: settings.flushAt,
flushInterval: settings.flushInterval,
shouldDisableSignalRedaction: this.redaction.getDisableSignalRedaction,
shouldDisableSignalsRedaction:
this.signalsDebug.getDisableSignalsRedaction,
shouldIngestSignals: () => {
if (this.signalsDebug.getEnableSignalsIngestion()) {
return true
}
if (!this.sampleSuccess) {
return false
}
return false
},
}
this.sandbox = {
functionHost: settings.functionHost,
Expand All @@ -70,6 +83,7 @@ export class SignalGlobalSettings {
public update({
edgeFnDownloadURL,
disallowListURLs,
sampleRate,
}: {
/**
* The URL to download the edge function from
Expand All @@ -79,58 +93,80 @@ export class SignalGlobalSettings {
* Add new URLs to the disallow list
*/
disallowListURLs: (string | undefined)[]
/**
* Sample rate to determine sending signals
*/
sampleRate?: number
}): void {
edgeFnDownloadURL && (this.sandbox.edgeFnDownloadURL = edgeFnDownloadURL)
this.network.networkSignalsFilterList.disallowed.addURLLike(
...disallowListURLs.filter(<T>(val: T): val is NonNullable<T> =>
Boolean(val)
)
)
if (sampleRate && Math.random() <= sampleRate) {
this.sampleSuccess = true
}
}
}

class SignalRedactionSettings {
class SignalsDebugSettings {
private static redactionKey = 'segment_signals_debug_redaction_disabled'
constructor(initialValue?: boolean) {
if (typeof initialValue === 'boolean') {
this.setDisableSignalRedaction(initialValue)
private static ingestionKey = 'segment_signals_debug_ingestion_enabled'
constructor(disableRedaction?: boolean, enableIngestion?: boolean) {
if (typeof disableRedaction === 'boolean') {
this.setDebugKey(SignalsDebugSettings.redactionKey, disableRedaction)
}
if (typeof enableIngestion === 'boolean') {
this.setDebugKey(SignalsDebugSettings.ingestionKey, enableIngestion)
}

// setting ?segment_signals_debug=true will disable redaction, and set a key in local storage
// setting ?segment_signals_debug=true will disable redaction, enable ingestion, and set keys in local storage
// this setting will persist across page loads (even if there is no query string)
// in order to clear the setting, user must set ?segment_signals_debug=false
const debugModeInQs = parseDebugModeQueryString()
logger.debug('debugMode is set to true via query string')
if (typeof debugModeInQs === 'boolean') {
this.setDisableSignalRedaction(debugModeInQs)
this.setDebugKey(SignalsDebugSettings.redactionKey, debugModeInQs)
this.setDebugKey(SignalsDebugSettings.ingestionKey, debugModeInQs)
}
}

setDisableSignalRedaction(shouldDisable: boolean) {
setDebugKey(key: string, enable: boolean) {
try {
if (shouldDisable) {
window.sessionStorage.setItem(
SignalRedactionSettings.redactionKey,
'true'
)
if (enable) {
window.sessionStorage.setItem(key, 'true')
} else {
logger.debug('Removing redaction key from storage')
window.sessionStorage.removeItem(SignalRedactionSettings.redactionKey)
logger.debug(`Removing debug key ${key} from storage`)
window.sessionStorage.removeItem(key)
}
} catch (e) {
logger.debug('Storage error', e)
}
}

getDisableSignalsRedaction() {
try {
const isEnabled = Boolean(
window.sessionStorage.getItem(SignalsDebugSettings.redactionKey)
)
if (isEnabled) {
logger.debug(`${SignalsDebugSettings.redactionKey}=true (app. storage)`)
return true
}
} catch (e) {
logger.debug('Storage error', e)
}
return false
}

getDisableSignalRedaction() {
getEnableSignalsIngestion() {
try {
const isDisabled = Boolean(
window.sessionStorage.getItem(SignalRedactionSettings.redactionKey)
const isEnabled = Boolean(
window.sessionStorage.getItem(SignalsDebugSettings.ingestionKey)
)
if (isDisabled) {
logger.debug(
`${SignalRedactionSettings.redactionKey}=true (app. storage)`
)
if (isEnabled) {
logger.debug(`${SignalsDebugSettings.ingestionKey}=true (app. storage)`)
return true
}
} catch (e) {
Expand Down
3 changes: 3 additions & 0 deletions packages/signals/signals/src/core/signals/signals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export class Signals implements ISignals {
analyticsService.instance.settings.apiHost,
analyticsService.instance.settings.cdnURL,
],
sampleRate:
analyticsService.instance.settings.cdnSettings
.autoInstrumentationSettings?.sampleRate ?? 0,
})

const sandbox = new Sandbox(
Expand Down
1 change: 1 addition & 0 deletions packages/signals/signals/src/plugin/signals-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class SignalsPlugin implements Plugin, SignalsAugmentedFunctionality {

this.signals = new Signals({
disableSignalsRedaction: settings.disableSignalsRedaction,
enableSignalsIngestion: settings.enableSignalsIngestion,
flushAt: settings.flushAt,
flushInterval: settings.flushInterval,
functionHost: settings.functionHost,
Expand Down
5 changes: 5 additions & 0 deletions packages/signals/signals/src/types/analytics-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ export type EdgeFnCDNSettings = {
downloadURL: string
}

export type AutoInstrumentationCDNSettings = {
sampleRate: number
}

export interface CDNSettings {
integrations: CDNSettingsIntegrations
edgeFunction?: EdgeFnCDNSettings | { [key: string]: never }
autoInstrumentationSettings?: AutoInstrumentationCDNSettings
}

export interface SegmentEventStub {
Expand Down
5 changes: 5 additions & 0 deletions packages/signals/signals/src/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export interface SignalsPluginSettingsConfig {
*/
disableSignalsRedaction?: boolean

/**
* Enable ingestion of signals
*/
enableSignalsIngestion?: boolean

/**
* Override signals API host
* @default signals.segment.io/v1
Expand Down

0 comments on commit 9e6db28

Please sign in to comment.