diff --git a/.changeset/nasty-cherries-burn.md b/.changeset/nasty-cherries-burn.md new file mode 100644 index 000000000..8480dce9d --- /dev/null +++ b/.changeset/nasty-cherries-burn.md @@ -0,0 +1,6 @@ +--- +'@segment/analytics-next': minor +'@segment/analytics-signals': minor +--- + +Add sampling logic and block non debug traffic diff --git a/packages/browser/src/browser/index.ts b/packages/browser/src/browser/index.ts index c9d1927ec..097175416 100644 --- a/packages/browser/src/browser/index.ts +++ b/packages/browser/src/browser/index.ts @@ -122,6 +122,13 @@ export interface CDNSettings { version: number } | {} + + /** + * Settings for auto instrumentation + */ + autoInstrumentationSettings?: { + sampleRate: number + } } export interface AnalyticsBrowserSettings { diff --git a/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts b/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts index 9fdfd3b74..f2b3004e6 100644 --- a/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts +++ b/packages/signals/signals-integration-tests/src/helpers/base-page-object.ts @@ -74,6 +74,7 @@ export class BasePage { ({ signalSettings }) => { window.signalsPlugin = new window.SignalsPlugin({ disableSignalsRedaction: true, + enableSignalsIngestion: true, ...signalSettings, }) window.analytics.load({ diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts index 0d301ce79..a44682508 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts @@ -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([ diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts new file mode 100644 index 000000000..0e5eddc38 --- /dev/null +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-ingestion.test.ts @@ -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) +}) diff --git a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts index 6540078bc..3dc8b67e4 100644 --- a/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts +++ b/packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts @@ -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([ @@ -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([ diff --git a/packages/signals/signals/src/core/client/__tests__/client.test.ts b/packages/signals/signals/src/core/client/__tests__/client.test.ts index 11255dd92..4990d0574 100644 --- a/packages/signals/signals/src/core/client/__tests__/client.test.ts +++ b/packages/signals/signals/src/core/client/__tests__/client.test.ts @@ -11,7 +11,9 @@ describe(SignalsIngestClient, () => { let client: SignalsIngestClient beforeEach(async () => { - client = new SignalsIngestClient() + client = new SignalsIngestClient({ + shouldIngestSignals: () => true, + }) await client.init({ writeKey: 'test' }) }) diff --git a/packages/signals/signals/src/core/client/index.ts b/packages/signals/signals/src/core/client/index.ts index 346c2c324..74f5b6752 100644 --- a/packages/signals/signals/src/core/client/index.ts +++ b/packages/signals/signals/src/core/client/index.ts @@ -7,14 +7,16 @@ 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) } } @@ -22,7 +24,8 @@ 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. @@ -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) { diff --git a/packages/signals/signals/src/core/signals/settings.ts b/packages/signals/signals/src/core/signals/settings.ts index 8fb52094f..6b6a96e3a 100644 --- a/packages/signals/signals/src/core/signals/settings.ts +++ b/packages/signals/signals/src/core/signals/settings.ts @@ -14,6 +14,7 @@ export type SignalsSettingsConfig = Pick< | 'flushAt' | 'flushInterval' | 'disableSignalsRedaction' + | 'enableSignalsIngestion' | 'networkSignalsAllowList' | 'networkSignalsDisallowList' | 'networkSignalsAllowSameDomain' @@ -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) { @@ -42,8 +44,9 @@ export class SignalGlobalSettings { ) } - this.redaction = new SignalRedactionSettings( - settings.disableSignalsRedaction + this.signalsDebug = new SignalsDebugSettings( + settings.disableSignalsRedaction, + settings.enableSignalsIngestion ) this.signalBuffer = { @@ -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, @@ -70,6 +83,7 @@ export class SignalGlobalSettings { public update({ edgeFnDownloadURL, disallowListURLs, + sampleRate, }: { /** * The URL to download the edge function from @@ -79,6 +93,10 @@ 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( @@ -86,51 +104,69 @@ export class SignalGlobalSettings { 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) { diff --git a/packages/signals/signals/src/core/signals/signals.ts b/packages/signals/signals/src/core/signals/signals.ts index 634210604..39438a274 100644 --- a/packages/signals/signals/src/core/signals/signals.ts +++ b/packages/signals/signals/src/core/signals/signals.ts @@ -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( diff --git a/packages/signals/signals/src/plugin/signals-plugin.ts b/packages/signals/signals/src/plugin/signals-plugin.ts index 7df1ed8d5..9ea239843 100644 --- a/packages/signals/signals/src/plugin/signals-plugin.ts +++ b/packages/signals/signals/src/plugin/signals-plugin.ts @@ -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, diff --git a/packages/signals/signals/src/types/analytics-api.ts b/packages/signals/signals/src/types/analytics-api.ts index cc84982ce..27353e959 100644 --- a/packages/signals/signals/src/types/analytics-api.ts +++ b/packages/signals/signals/src/types/analytics-api.ts @@ -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 { diff --git a/packages/signals/signals/src/types/settings.ts b/packages/signals/signals/src/types/settings.ts index ca0f78fb6..3e20d02da 100644 --- a/packages/signals/signals/src/types/settings.ts +++ b/packages/signals/signals/src/types/settings.ts @@ -23,6 +23,11 @@ export interface SignalsPluginSettingsConfig { */ disableSignalsRedaction?: boolean + /** + * Enable ingestion of signals + */ + enableSignalsIngestion?: boolean + /** * Override signals API host * @default signals.segment.io/v1