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

Signals sampling #1166

Merged
merged 23 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 21 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
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
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need a changeset for both signals and analytics =)

}

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,37 @@
import { test, expect } from '@playwright/test'
import { waitForCondition } from '../../helpers/playwright-utils'
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)
})
})

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(),
])

await waitForCondition(
() => indexPage.signalsAPI.getEvents('interaction').length > 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is and everything below it is needed -- as long as signals api has flushed, test is good!

{ errorMessage: 'No interaction signals found' }
)

const interactionSignals = indexPage.signalsAPI.getEvents('interaction')
expect(interactionSignals.length > 0)
})
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
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this | undefined / optional, so update can be used atomically (one day?)

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
Loading