Skip to content

Commit 9e6db28

Browse files
Signals sampling (#1166)
1 parent 3b9a9ee commit 9e6db28

File tree

13 files changed

+136
-31
lines changed

13 files changed

+136
-31
lines changed

.changeset/nasty-cherries-burn.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@segment/analytics-next': minor
3+
'@segment/analytics-signals': minor
4+
---
5+
6+
Add sampling logic and block non debug traffic

packages/browser/src/browser/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ export interface CDNSettings {
122122
version: number
123123
}
124124
| {}
125+
126+
/**
127+
* Settings for auto instrumentation
128+
*/
129+
autoInstrumentationSettings?: {
130+
sampleRate: number
131+
}
125132
}
126133

127134
export interface AnalyticsBrowserSettings {

packages/signals/signals-integration-tests/src/helpers/base-page-object.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class BasePage {
7474
({ signalSettings }) => {
7575
window.signalsPlugin = new window.SignalsPlugin({
7676
disableSignalsRedaction: true,
77+
enableSignalsIngestion: true,
7778
...signalSettings,
7879
})
7980
window.analytics.load({

packages/signals/signals-integration-tests/src/tests/signals-vanilla/change-input.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ test('Collecting signals whenever a user enters text input', async ({
1414
*/
1515
await indexPage.loadAndWait(page, basicEdgeFn, {
1616
disableSignalsRedaction: true,
17+
enableSignalsIngestion: true,
1718
})
1819

1920
await Promise.all([
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { test, expect } from '@playwright/test'
2+
import { IndexPage } from './index-page'
3+
4+
const indexPage = new IndexPage()
5+
6+
const basicEdgeFn = `const processSignal = (signal) => {}`
7+
8+
test('ingestion not enabled -> will not send the signal', async ({ page }) => {
9+
await indexPage.loadAndWait(page, basicEdgeFn, {
10+
enableSignalsIngestion: false,
11+
})
12+
13+
await indexPage.fillNameInput('John Doe')
14+
await indexPage.waitForSignalsApiFlush().catch(() => {
15+
expect(true).toBe(true)
16+
})
17+
})
18+
19+
test('ingestion enabled -> will send the signal', async ({ page }) => {
20+
await indexPage.loadAndWait(page, basicEdgeFn, {
21+
enableSignalsIngestion: true,
22+
})
23+
24+
await Promise.all([
25+
indexPage.fillNameInput('John Doe'),
26+
indexPage.waitForSignalsApiFlush(),
27+
])
28+
29+
expect(true).toBe(true)
30+
})

packages/signals/signals-integration-tests/src/tests/signals-vanilla/signals-redaction.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ test('redaction enabled -> will XXX the value of text input', async ({
1111
}) => {
1212
await indexPage.loadAndWait(page, basicEdgeFn, {
1313
disableSignalsRedaction: false,
14+
enableSignalsIngestion: true,
1415
})
1516

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

4547
await Promise.all([

packages/signals/signals/src/core/client/__tests__/client.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ describe(SignalsIngestClient, () => {
1111
let client: SignalsIngestClient
1212

1313
beforeEach(async () => {
14-
client = new SignalsIngestClient()
14+
client = new SignalsIngestClient({
15+
shouldIngestSignals: () => true,
16+
})
1517
await client.init({ writeKey: 'test' })
1618
})
1719

packages/signals/signals/src/core/client/index.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,25 @@ export class SignalsIngestSettings {
77
flushAt: number
88
flushInterval: number
99
apiHost: string
10-
shouldDisableSignalRedaction: () => boolean
10+
shouldDisableSignalsRedaction: () => boolean
11+
shouldIngestSignals: () => boolean
1112
writeKey?: string
1213
constructor(settings: SignalsIngestSettingsConfig) {
1314
this.flushAt = settings.flushAt ?? 5
1415
this.apiHost = settings.apiHost ?? 'signals.segment.io/v1'
1516
this.flushInterval = settings.flushInterval ?? 2000
16-
this.shouldDisableSignalRedaction =
17-
settings.shouldDisableSignalRedaction ?? (() => false)
17+
this.shouldDisableSignalsRedaction =
18+
settings.shouldDisableSignalsRedaction ?? (() => false)
19+
this.shouldIngestSignals = settings.shouldIngestSignals ?? (() => false)
1820
}
1921
}
2022

2123
export interface SignalsIngestSettingsConfig {
2224
apiHost?: string
2325
flushAt?: number
2426
flushInterval?: number
25-
shouldDisableSignalRedaction?: () => boolean
27+
shouldDisableSignalsRedaction?: () => boolean
28+
shouldIngestSignals?: () => boolean
2629
}
2730
/**
2831
* This currently just uses the Segment analytics-next library to send signals.
@@ -73,7 +76,10 @@ export class SignalsIngestClient {
7376
if (!this.analytics) {
7477
throw new Error('Please initialize before calling this method.')
7578
}
76-
const disableRedaction = this.settings.shouldDisableSignalRedaction()
79+
if (!this.settings.shouldIngestSignals()) {
80+
return
81+
}
82+
const disableRedaction = this.settings.shouldDisableSignalsRedaction()
7783
const cleanSignal = disableRedaction ? signal : redactSignalData(signal)
7884

7985
if (disableRedaction) {

packages/signals/signals/src/core/signals/settings.ts

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type SignalsSettingsConfig = Pick<
1414
| 'flushAt'
1515
| 'flushInterval'
1616
| 'disableSignalsRedaction'
17+
| 'enableSignalsIngestion'
1718
| 'networkSignalsAllowList'
1819
| 'networkSignalsDisallowList'
1920
| 'networkSignalsAllowSameDomain'
@@ -33,7 +34,8 @@ export class SignalGlobalSettings {
3334
ingestClient: SignalsIngestSettingsConfig
3435
network: NetworkSettingsConfig
3536

36-
private redaction = new SignalRedactionSettings()
37+
private sampleSuccess = false
38+
private signalsDebug = new SignalsDebugSettings()
3739

3840
constructor(settings: SignalsSettingsConfig) {
3941
if (settings.maxBufferSize && settings.signalStorage) {
@@ -42,8 +44,9 @@ export class SignalGlobalSettings {
4244
)
4345
}
4446

45-
this.redaction = new SignalRedactionSettings(
46-
settings.disableSignalsRedaction
47+
this.signalsDebug = new SignalsDebugSettings(
48+
settings.disableSignalsRedaction,
49+
settings.enableSignalsIngestion
4750
)
4851

4952
this.signalBuffer = {
@@ -54,7 +57,17 @@ export class SignalGlobalSettings {
5457
apiHost: settings.apiHost,
5558
flushAt: settings.flushAt,
5659
flushInterval: settings.flushInterval,
57-
shouldDisableSignalRedaction: this.redaction.getDisableSignalRedaction,
60+
shouldDisableSignalsRedaction:
61+
this.signalsDebug.getDisableSignalsRedaction,
62+
shouldIngestSignals: () => {
63+
if (this.signalsDebug.getEnableSignalsIngestion()) {
64+
return true
65+
}
66+
if (!this.sampleSuccess) {
67+
return false
68+
}
69+
return false
70+
},
5871
}
5972
this.sandbox = {
6073
functionHost: settings.functionHost,
@@ -70,6 +83,7 @@ export class SignalGlobalSettings {
7083
public update({
7184
edgeFnDownloadURL,
7285
disallowListURLs,
86+
sampleRate,
7387
}: {
7488
/**
7589
* The URL to download the edge function from
@@ -79,58 +93,80 @@ export class SignalGlobalSettings {
7993
* Add new URLs to the disallow list
8094
*/
8195
disallowListURLs: (string | undefined)[]
96+
/**
97+
* Sample rate to determine sending signals
98+
*/
99+
sampleRate?: number
82100
}): void {
83101
edgeFnDownloadURL && (this.sandbox.edgeFnDownloadURL = edgeFnDownloadURL)
84102
this.network.networkSignalsFilterList.disallowed.addURLLike(
85103
...disallowListURLs.filter(<T>(val: T): val is NonNullable<T> =>
86104
Boolean(val)
87105
)
88106
)
107+
if (sampleRate && Math.random() <= sampleRate) {
108+
this.sampleSuccess = true
109+
}
89110
}
90111
}
91112

92-
class SignalRedactionSettings {
113+
class SignalsDebugSettings {
93114
private static redactionKey = 'segment_signals_debug_redaction_disabled'
94-
constructor(initialValue?: boolean) {
95-
if (typeof initialValue === 'boolean') {
96-
this.setDisableSignalRedaction(initialValue)
115+
private static ingestionKey = 'segment_signals_debug_ingestion_enabled'
116+
constructor(disableRedaction?: boolean, enableIngestion?: boolean) {
117+
if (typeof disableRedaction === 'boolean') {
118+
this.setDebugKey(SignalsDebugSettings.redactionKey, disableRedaction)
119+
}
120+
if (typeof enableIngestion === 'boolean') {
121+
this.setDebugKey(SignalsDebugSettings.ingestionKey, enableIngestion)
97122
}
98123

99-
// setting ?segment_signals_debug=true will disable redaction, and set a key in local storage
124+
// setting ?segment_signals_debug=true will disable redaction, enable ingestion, and set keys in local storage
100125
// this setting will persist across page loads (even if there is no query string)
101126
// in order to clear the setting, user must set ?segment_signals_debug=false
102127
const debugModeInQs = parseDebugModeQueryString()
103128
logger.debug('debugMode is set to true via query string')
104129
if (typeof debugModeInQs === 'boolean') {
105-
this.setDisableSignalRedaction(debugModeInQs)
130+
this.setDebugKey(SignalsDebugSettings.redactionKey, debugModeInQs)
131+
this.setDebugKey(SignalsDebugSettings.ingestionKey, debugModeInQs)
106132
}
107133
}
108134

109-
setDisableSignalRedaction(shouldDisable: boolean) {
135+
setDebugKey(key: string, enable: boolean) {
110136
try {
111-
if (shouldDisable) {
112-
window.sessionStorage.setItem(
113-
SignalRedactionSettings.redactionKey,
114-
'true'
115-
)
137+
if (enable) {
138+
window.sessionStorage.setItem(key, 'true')
116139
} else {
117-
logger.debug('Removing redaction key from storage')
118-
window.sessionStorage.removeItem(SignalRedactionSettings.redactionKey)
140+
logger.debug(`Removing debug key ${key} from storage`)
141+
window.sessionStorage.removeItem(key)
142+
}
143+
} catch (e) {
144+
logger.debug('Storage error', e)
145+
}
146+
}
147+
148+
getDisableSignalsRedaction() {
149+
try {
150+
const isEnabled = Boolean(
151+
window.sessionStorage.getItem(SignalsDebugSettings.redactionKey)
152+
)
153+
if (isEnabled) {
154+
logger.debug(`${SignalsDebugSettings.redactionKey}=true (app. storage)`)
155+
return true
119156
}
120157
} catch (e) {
121158
logger.debug('Storage error', e)
122159
}
160+
return false
123161
}
124162

125-
getDisableSignalRedaction() {
163+
getEnableSignalsIngestion() {
126164
try {
127-
const isDisabled = Boolean(
128-
window.sessionStorage.getItem(SignalRedactionSettings.redactionKey)
165+
const isEnabled = Boolean(
166+
window.sessionStorage.getItem(SignalsDebugSettings.ingestionKey)
129167
)
130-
if (isDisabled) {
131-
logger.debug(
132-
`${SignalRedactionSettings.redactionKey}=true (app. storage)`
133-
)
168+
if (isEnabled) {
169+
logger.debug(`${SignalsDebugSettings.ingestionKey}=true (app. storage)`)
134170
return true
135171
}
136172
} catch (e) {

packages/signals/signals/src/core/signals/signals.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ export class Signals implements ISignals {
9191
analyticsService.instance.settings.apiHost,
9292
analyticsService.instance.settings.cdnURL,
9393
],
94+
sampleRate:
95+
analyticsService.instance.settings.cdnSettings
96+
.autoInstrumentationSettings?.sampleRate ?? 0,
9497
})
9598

9699
const sandbox = new Sandbox(

0 commit comments

Comments
 (0)