-
Notifications
You must be signed in to change notification settings - Fork 135
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
08b6741
commit 54d3031
Showing
5 changed files
with
138 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { sampleByDistinctId, sampleByEvent, sampleBySessionId } from '../../utils/before-capture.utils' | ||
import { CaptureResult } from '../../types' | ||
import { isNull } from '../../utils/type-utils' | ||
|
||
function expectRoughlyFiftyPercent(emittedEvents: any[]) { | ||
expect(emittedEvents.length).toBeGreaterThanOrEqual(40) | ||
expect(emittedEvents.length).toBeLessThanOrEqual(60) | ||
} | ||
|
||
describe('before capture utils', () => { | ||
it('can sample by event name', () => { | ||
const sampleFn = sampleByEvent(['$autocapture'], 50) | ||
const results = [] | ||
Array.from({ length: 100 }).forEach(() => { | ||
const captureResult = { event: '$autocapture' } as unknown as CaptureResult | ||
results.push(sampleFn(captureResult)) | ||
}) | ||
const emittedEvents = results.filter((r) => !isNull(r)) | ||
expectRoughlyFiftyPercent(emittedEvents) | ||
}) | ||
|
||
it('can sample by distinct id', () => { | ||
const sampleFn = sampleByDistinctId(50) | ||
const results = [] | ||
const distinct_id_one = 'user-1' | ||
const distinct_id_two = 'user-that-hashes-to-no-events' | ||
Array.from({ length: 100 }).forEach(() => { | ||
;[distinct_id_one, distinct_id_two].forEach((distinct_id) => { | ||
const captureResult = { properties: { distinct_id } } as unknown as CaptureResult | ||
results.push(sampleFn(captureResult)) | ||
}) | ||
}) | ||
const distinctIdOneEvents = results.filter((r) => !isNull(r) && r.properties.distinct_id === distinct_id_one) | ||
const distinctIdTwoEvents = results.filter((r) => !isNull(r) && r.properties.distinct_id === distinct_id_two) | ||
|
||
expect(distinctIdOneEvents.length).toBe(100) | ||
expect(distinctIdTwoEvents.length).toBe(0) | ||
}) | ||
|
||
it('can sample by session id', () => { | ||
const sampleFn = sampleBySessionId(50) | ||
const results = [] | ||
const session_id_one = 'a-session-id' | ||
const session_id_two = 'id-that-hashes-to-not-sending-events' | ||
Array.from({ length: 100 }).forEach(() => { | ||
;[session_id_one, session_id_two].forEach((session_id) => { | ||
const captureResult = { properties: { $session_id: session_id } } as unknown as CaptureResult | ||
results.push(sampleFn(captureResult)) | ||
}) | ||
}) | ||
const sessionIdOneEvents = results.filter((r) => !isNull(r) && r.properties.$session_id === session_id_one) | ||
const sessionIdTwoEvents = results.filter((r) => !isNull(r) && r.properties.$session_id === session_id_two) | ||
|
||
expect(sessionIdOneEvents.length).toBe(100) | ||
expect(sessionIdTwoEvents.length).toBe(0) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { clampToRange } from './number-utils' | ||
import { CaptureResult, KnownEventName } from '../types' | ||
import { includes } from './index' | ||
|
||
function simpleHash(str: string) { | ||
let hash = 0 | ||
for (let i = 0; i < str.length; i++) { | ||
hash = (hash << 5) - hash + str.charCodeAt(i) // (hash * 31) + char code | ||
hash |= 0 // Convert to 32bit integer | ||
} | ||
return Math.abs(hash) | ||
} | ||
|
||
/** | ||
* An implementation of sampling that samples based on the distinct ID. | ||
* Can be used to create a beforeCapture fn for a PostHog instance. | ||
* | ||
* Causes roughly 50% of distinct ids to have events sent. | ||
* Not 50% of events for each distinct id. | ||
* | ||
* @param percent a number from 0 to 100, 100 means never sample, 0 means never send the event | ||
*/ | ||
export function sampleByDistinctId(percent: number): (c: CaptureResult) => CaptureResult | null { | ||
return (captureResult: CaptureResult): CaptureResult | null => { | ||
const hash = simpleHash(captureResult.properties.distinct_id) | ||
return hash % 100 < clampToRange(percent, 0, 100) ? captureResult : null | ||
} | ||
} | ||
|
||
/** | ||
* An implementation of sampling that samples based on the session ID. | ||
* Can be used to create a beforeCapture fn for a PostHog instance. | ||
* | ||
* Causes roughly 50% of sessions to have events sent. | ||
* Not 50% of events for each session. | ||
* | ||
* @param percent a number from 0 to 100, 100 means never sample, 0 means never send the event | ||
*/ | ||
export function sampleBySessionId(percent: number): (c: CaptureResult) => CaptureResult | null { | ||
return (captureResult: CaptureResult): CaptureResult | null => { | ||
const hash = simpleHash(captureResult.properties.$session_id) | ||
return hash % 100 < clampToRange(percent, 0, 100) ? captureResult : null | ||
} | ||
} | ||
|
||
/** | ||
* An implementation of sampling that samples based on the event name. | ||
* Can be used to create a beforeCapture fn for a PostHog instance. | ||
* | ||
* @param eventNames an array of event names to sample, sampling is applied across events not per event name | ||
* @param percent a number from 0 to 100, 100 means never sample, 0 means never send the event | ||
*/ | ||
export function sampleByEvent( | ||
eventNames: KnownEventName[], | ||
percent: number | ||
): (c: CaptureResult) => CaptureResult | null { | ||
return (captureResult: CaptureResult): CaptureResult | null => { | ||
if (!includes(eventNames, captureResult.event)) { | ||
return captureResult | ||
} | ||
|
||
return Math.random() * 100 < clampToRange(percent, 0, 100) ? captureResult : null | ||
} | ||
} |