Skip to content

Commit

Permalink
feat: allow sampling based on decide response (#839)
Browse files Browse the repository at this point in the history
* feat: allow sampling based on decide response

* extract type

* move the comment

* quick version

* maybe nicer

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* convert test file to TS

* use real persistence code

* can store session minimum duration if received

* type safety for decide response in tests

* don't flush until minimum duration

* move our files into an extension directory

* no need to put sampling metadata on

* fix

* fix first pass minimum duration fitler

* log when delaying

* log when delaying

* remove noisy log

* can calculate rather than store status so its all in one place

* linked flags control recordings

* fix

* fix

* fix

* fix

* Fix

* Update src/extensions/replay/sessionrecording.ts

Co-authored-by: Ben White <[email protected]>

* Update src/extensions/replay/sessionrecording.ts

Co-authored-by: Ben White <[email protected]>

* with a comment for the future traveller

* rename refactor

* more hidden

* fewer getters

* fix

---------

Co-authored-by: Ben White <[email protected]>
  • Loading branch information
pauldambra and benjackwhite authored Oct 24, 2023
1 parent 9cb5efd commit 7606b0f
Show file tree
Hide file tree
Showing 23 changed files with 825 additions and 378 deletions.
7 changes: 4 additions & 3 deletions src/__tests__/extensions/exceptions/error-conversion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
ErrorProperties,
unhandledRejectionToProperties,
} from '../../../extensions/exceptions/error-conversion'
import { _isNull } from '../../../utils'

// ugh, jest
// can't reference PromiseRejectionEvent to construct it 🤷
Expand Down Expand Up @@ -61,7 +62,7 @@ describe('Error conversion', () => {
const error = new Error('oh no an error has happened')

const errorProperties = errorToProperties(['something', undefined, undefined, undefined, error])
if (errorProperties === null) {
if (_isNull(errorProperties)) {
throw new Error("this mustn't be null")
}

Expand Down Expand Up @@ -91,7 +92,7 @@ describe('Error conversion', () => {
const event = new DOMException('oh no disaster', 'dom-exception')
const errorProperties = errorToProperties([event as unknown as Event])

if (errorProperties === null) {
if (_isNull(errorProperties)) {
throw new Error("this mustn't be null")
}

Expand All @@ -107,7 +108,7 @@ describe('Error conversion', () => {
const event = new ErrorEvent('oh no an error event', { error: new Error('the real error is hidden inside') })

const errorProperties = errorToProperties([event as unknown as Event])
if (errorProperties === null) {
if (_isNull(errorProperties)) {
throw new Error("this mustn't be null")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
CONSOLE_LOG_PLUGIN_NAME,
PLUGIN_EVENT_TYPE,
FULL_SNAPSHOT_EVENT_TYPE,
} from '../../extensions/sessionrecording-utils'
import { largeString, threeMBAudioURI, threeMBImageURI } from './test_data/sessionrecording-utils-test-data'
} from '../../../extensions/replay/sessionrecording-utils'
import { largeString, threeMBAudioURI, threeMBImageURI } from '../test_data/sessionrecording-utils-test-data'
import { eventWithTime } from '@rrweb/types'

describe(`SessionRecording utility functions`, () => {
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable compat/compat */

import { WebPerformanceObserver } from '../../extensions/web-performance'
import { PostHog } from '../../posthog-core'
import { NetworkRequest, PostHogConfig } from '../../types'
import { WebPerformanceObserver } from '../../../extensions/replay/web-performance'
import { PostHog } from '../../../posthog-core'
import { NetworkRequest, PostHogConfig } from '../../../types'

const createMockPerformanceEntry = (overrides: Partial<PerformanceEntry> = {}): PerformanceEntry => {
const entry = {
Expand Down
5 changes: 3 additions & 2 deletions src/__tests__/gdpr-utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sinon from 'sinon'

import * as gdpr from '../gdpr-utils'
import { _isNull } from '../utils'

const TOKENS = [
`test-token`,
Expand Down Expand Up @@ -29,13 +30,13 @@ function forPersistenceTypes(runTests) {

function assertPersistenceValue(persistenceType, token, value, persistencePrefix = DEFAULT_PERSISTENCE_PREFIX) {
if (persistenceType === `cookie`) {
if (value === null) {
if (_isNull(value)) {
expect(document.cookie).not.toContain(token)
} else {
expect(document.cookie).toContain(token + `=${value}`)
}
} else {
if (value === null) {
if (_isNull(value)) {
expect(window.localStorage.getItem(persistencePrefix + token)).toBeNull()
} else {
expect(window.localStorage.getItem(persistencePrefix + token)).toBe(`${value}`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,66 @@
/// <reference lib="dom" />
import { PostHogPersistence } from '../posthog-persistence'
import { SESSION_ID, USER_STATE } from '../constants'

given('lib', () => new PostHogPersistence({ name: 'bla', persistence: 'cookie' }))
import { PostHogConfig } from '../types'
import Mock = jest.Mock

let referrer = '' // No referrer by default
Object.defineProperty(document, 'referrer', { get: () => referrer })

function makePostHogConfig(name: string, persistenceMode: string): PostHogConfig {
return <PostHogConfig>{
name,
persistence: persistenceMode as 'cookie' | 'localStorage' | 'localStorage+cookie' | 'memory' | 'sessionStorage',
}
}

describe('persistence', () => {
let library: PostHogPersistence

afterEach(() => {
given.lib.clear()
library.clear()
document.cookie = ''
referrer = ''
})

describe.each([`cookie`, `localStorage`, `localStorage+cookie`])('persistence modes: %p', (persistenceMode) => {
// Common tests for all storage modes
beforeEach(() => {
given('lib', () => new PostHogPersistence({ name: 'test', persistence: persistenceMode }))
given.lib.clear()
library = new PostHogPersistence(makePostHogConfig('test', persistenceMode))
library.clear()
})

it('should register_once', () => {
given.lib.register_once({ distinct_id: 'hi', test_prop: 'test_val' })
library.register_once({ distinct_id: 'hi', test_prop: 'test_val' }, undefined, undefined)

let lib2 = new PostHogPersistence({ name: 'test', persistence: persistenceMode })
const lib2 = new PostHogPersistence(makePostHogConfig('test', persistenceMode))
expect(lib2.props).toEqual({ distinct_id: 'hi', test_prop: 'test_val' })
})

it('should save user state', () => {
let lib = new PostHogPersistence({ name: 'bla', persistence: 'cookie' })
const lib = new PostHogPersistence(makePostHogConfig('bla', persistenceMode))
lib.set_user_state('identified')
expect(lib.props[USER_STATE]).toEqual('identified')
})

it('can load user state', () => {
let lib = new PostHogPersistence({ name: 'bla', persistence: 'cookie' })
const lib = new PostHogPersistence(makePostHogConfig('bla', persistenceMode))
lib.set_user_state('identified')
expect(lib.get_user_state()).toEqual('identified')
})

it('has user state as a reserved property key', () => {
let lib = new PostHogPersistence({ name: 'bla', persistence: 'cookie' })
const lib = new PostHogPersistence(makePostHogConfig('bla', persistenceMode))
lib.register({ distinct_id: 'testy', test_prop: 'test_value' })
lib.set_user_state('identified')
expect(lib.properties()).toEqual({ distinct_id: 'testy', test_prop: 'test_value' })
})

it(`should only call save if props changes`, () => {
let lib = new PostHogPersistence({ name: 'test', persistence: 'localStorage+cookie' })
const lib = new PostHogPersistence(makePostHogConfig('test', 'localStorage+cookie'))
lib.register({ distinct_id: 'hi', test_prop: 'test_val' })
lib.save = jest.fn()
const saveMock: Mock = jest.fn()
lib.save = saveMock

lib.register({ distinct_id: 'hi', test_prop: 'test_val' })
lib.register({})
Expand All @@ -58,41 +69,41 @@ describe('persistence', () => {

lib.register({ distinct_id: 'hi2' })
expect(lib.save).toHaveBeenCalledTimes(1)
lib.save.mockClear()
saveMock.mockClear()

lib.register({ new_key: '1234' })
expect(lib.save).toHaveBeenCalledTimes(1)
lib.save.mockClear()
saveMock.mockClear()
})

it('should set direct referrer', () => {
referrer = ''
given.lib.update_referrer_info()
library.update_referrer_info()

expect(given.lib.props['$referring_domain']).toBe('$direct')
expect(given.lib.props['$referrer']).toBe('$direct')
expect(library.props['$referring_domain']).toBe('$direct')
expect(library.props['$referrer']).toBe('$direct')
})

it('should set external referrer', () => {
referrer = 'https://www.google.com'
given.lib.update_referrer_info()
library.update_referrer_info()

expect(given.lib.props['$referring_domain']).toBe('www.google.com')
expect(given.lib.props['$referrer']).toBe('https://www.google.com')
expect(library.props['$referring_domain']).toBe('www.google.com')
expect(library.props['$referrer']).toBe('https://www.google.com')
})

it('should set internal referrer', () => {
referrer = 'https://hedgebox.net/files/abc.png'
given.lib.update_referrer_info()
library.update_referrer_info()

expect(given.lib.props['$referring_domain']).toBe('hedgebox.net')
expect(given.lib.props['$referrer']).toBe('https://hedgebox.net/files/abc.png')
expect(library.props['$referring_domain']).toBe('hedgebox.net')
expect(library.props['$referrer']).toBe('https://hedgebox.net/files/abc.png')
})

it('extracts enabled feature flags', () => {
given.lib.register({ $enabled_feature_flags: { flag: 'variant', other: true } })
expect(given.lib.props['$enabled_feature_flags']).toEqual({ flag: 'variant', other: true })
expect(given.lib.properties()).toEqual({
library.register({ $enabled_feature_flags: { flag: 'variant', other: true } })
expect(library.props['$enabled_feature_flags']).toEqual({ flag: 'variant', other: true })
expect(library.properties()).toEqual({
'$feature/flag': 'variant',
'$feature/other': true,
})
Expand All @@ -101,27 +112,27 @@ describe('persistence', () => {

describe('localStorage+cookie', () => {
it('should migrate data from cookies to localStorage', () => {
let lib = new PostHogPersistence({ name: 'bla', persistence: 'cookie' })
lib.register_once({ distinct_id: 'testy', test_prop: 'test_value' })
const lib = new PostHogPersistence(makePostHogConfig('bla', 'cookie'))
lib.register_once({ distinct_id: 'testy', test_prop: 'test_value' }, undefined, undefined)
expect(document.cookie).toContain(
'ph__posthog=%7B%22distinct_id%22%3A%22testy%22%2C%22test_prop%22%3A%22test_value%22%7D'
)
let lib2 = new PostHogPersistence({ name: 'bla', persistence: 'localStorage+cookie' })
const lib2 = new PostHogPersistence(makePostHogConfig('bla', 'localStorage+cookie'))
expect(document.cookie).toContain('ph__posthog=%7B%22distinct_id%22%3A%22testy%22%7D')
lib2.register({ test_prop2: 'test_val', distinct_id: 'test2' })
expect(document.cookie).toContain('ph__posthog=%7B%22distinct_id%22%3A%22test2%22%7D')
expect(lib2.props).toEqual({ distinct_id: 'test2', test_prop: 'test_value', test_prop2: 'test_val' })
lib2.remove('ph__posthog')
lib2.remove()
expect(localStorage.getItem('ph__posthog')).toEqual(null)
expect(document.cookie).toEqual('')
})

it(`should additionally store certain values in cookies if localStorage+cookie`, () => {
expect(document.cookie).toEqual('')

const encode = (props) => encodeURIComponent(JSON.stringify(props))
const encode = (props: any) => encodeURIComponent(JSON.stringify(props))

let lib = new PostHogPersistence({ name: 'test', persistence: 'localStorage+cookie' })
const lib = new PostHogPersistence(makePostHogConfig('test', 'localStorage+cookie'))
lib.register({ distinct_id: 'test', test_prop: 'test_val' })
expect(document.cookie).toContain(
`ph__posthog=${encode({
Expand All @@ -147,7 +158,7 @@ describe('persistence', () => {
// Clear localstorage to simulate being on a different domain
localStorage.clear()

const newLib = new PostHogPersistence({ name: 'test', persistence: 'localStorage+cookie' })
const newLib = new PostHogPersistence(makePostHogConfig('test', 'localStorage+cookie'))

expect(newLib.props).toEqual({
distinct_id: 'test',
Expand Down
2 changes: 1 addition & 1 deletion src/__tests__/retry-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { pickNextRetryDelay, RetryQueue } from '../retry-queue'
import * as SendRequest from '../send-request'
import { RateLimiter } from '../rate-limiter'
import { SESSION_RECORDING_BATCH_KEY } from '../extensions/sessionrecording'
import { SESSION_RECORDING_BATCH_KEY } from '../extensions/replay/sessionrecording'

const EPOCH = 1_600_000_000
const defaultRequestOptions = {
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const SESSION_RECORDING_ENABLED_SERVER_SIDE = '$session_recording_enabled
export const CONSOLE_LOG_RECORDING_ENABLED_SERVER_SIDE = '$console_log_recording_enabled_server_side'
export const SESSION_RECORDING_RECORDER_VERSION_SERVER_SIDE = '$session_recording_recorder_version_server_side' // follows rrweb versioning
export const SESSION_ID = '$sesid'
export const SESSION_RECORDING_IS_SAMPLED = '$session_is_sampled'
export const ENABLED_FEATURE_FLAGS = '$enabled_feature_flags'
export const PERSISTENCE_EARLY_ACCESS_FEATURES = '$early_access_features'
export const STORED_PERSON_PROPERTIES_KEY = '$stored_person_properties'
Expand Down
13 changes: 4 additions & 9 deletions src/extensions/exceptions/exception-autocapture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { logger, window } from '../../utils'
import { _isArray, _isUndefined, logger, window } from '../../utils'
import { PostHog } from '../../posthog-core'
import { DecideResponse, Properties } from '../../types'
import { ErrorEventArgs, ErrorProperties, errorToProperties, unhandledRejectionToProperties } from './error-conversion'
Expand Down Expand Up @@ -66,13 +66,13 @@ export class ExceptionObserver {
}

stopCapturing() {
if (this.originalOnErrorHandler !== undefined) {
if (!_isUndefined(this.originalOnErrorHandler)) {
window.onerror = this.originalOnErrorHandler
this.originalOnErrorHandler = null
}
delete (window.onerror as any)?.__POSTHOG_INSTRUMENTED__

if (this.originalOnUnhandledRejectionHandler !== undefined) {
if (!_isUndefined(this.originalOnUnhandledRejectionHandler)) {
window.onunhandledrejection = this.originalOnUnhandledRejectionHandler
this.originalOnUnhandledRejectionHandler = null
}
Expand All @@ -93,7 +93,7 @@ export class ExceptionObserver {
if (
!isPrimitive(autocaptureExceptionsResponse) &&
'errors_to_ignore' in autocaptureExceptionsResponse &&
Array.isArray(autocaptureExceptionsResponse.errors_to_ignore)
_isArray(autocaptureExceptionsResponse.errors_to_ignore)
) {
const dropRules = autocaptureExceptionsResponse.errors_to_ignore

Expand All @@ -105,11 +105,6 @@ export class ExceptionObserver {
if (this.isEnabled()) {
this.startCapturing()
this.debugLog('Remote config for exception autocapture is enabled, starting', autocaptureExceptionsResponse)
} else {
this.debugLog(
'Remote config for exception autocapture is disabled, not starting',
autocaptureExceptionsResponse
)
}
}

Expand Down
6 changes: 4 additions & 2 deletions src/extensions/exceptions/stack-trace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

import { _isUndefined } from '../../utils'

const WEBPACK_ERROR_REGEXP = /\(error: (.*)\)/
const STACKTRACE_FRAME_LIMIT = 50

Expand Down Expand Up @@ -62,11 +64,11 @@ function createFrame(filename: string, func: string, lineno?: number, colno?: nu
in_app: true, // All browser frames are considered in_app
}

if (lineno !== undefined) {
if (!_isUndefined(lineno)) {
frame.lineno = lineno
}

if (colno !== undefined) {
if (!_isUndefined(colno)) {
frame.colno = colno
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
mutationCallbackParam,
} from '@rrweb/types'
import type { Mirror, MaskInputOptions, MaskInputFn, MaskTextFn, SlimDOMOptions, DataURLOptions } from 'rrweb-snapshot'
import { _isObject } from '../utils'
import { _isObject } from '../../utils'

export const replacementImageURI =
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSJibGFjayIvPgo8cGF0aCBkPSJNOCAwSDE2TDAgMTZWOEw4IDBaIiBmaWxsPSIjMkQyRDJEIi8+CjxwYXRoIGQ9Ik0xNiA4VjE2SDhMMTYgOFoiIGZpbGw9IiMyRDJEMkQiLz4KPC9zdmc+Cg=='
Expand Down
Loading

0 comments on commit 7606b0f

Please sign in to comment.