Skip to content

Commit

Permalink
Started adding alternative loader
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite committed Dec 3, 2024
1 parent 9b94afd commit 57d6b77
Show file tree
Hide file tree
Showing 14 changed files with 128 additions and 37 deletions.
4 changes: 2 additions & 2 deletions src/autocapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
splitClassString,
} from './autocapture-utils'
import RageClick from './extensions/rageclick'
import { AutocaptureConfig, COPY_AUTOCAPTURE_EVENT, DecideResponse, EventName, Properties } from './types'
import { AutocaptureConfig, COPY_AUTOCAPTURE_EVENT, EventName, Properties, RemoteConfig } from './types'
import { PostHog } from './posthog-core'
import { AUTOCAPTURE_DISABLED_SERVER_SIDE } from './constants'

Expand Down Expand Up @@ -286,7 +286,7 @@ export class Autocapture {
}
}

public afterDecideResponse(response: DecideResponse) {
public onRemoteConfig(response: RemoteConfig) {
if (response.elementsChainAsString) {
this._elementsChainAsString = response.elementsChainAsString
}
Expand Down
73 changes: 71 additions & 2 deletions src/decide.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,68 @@
import { PostHog } from './posthog-core'
import { Compression, DecideResponse } from './types'
import { Compression, DecideResponse, RemoteConfig } from './types'
import { STORED_GROUP_PROPERTIES_KEY, STORED_PERSON_PROPERTIES_KEY } from './constants'

import { logger } from './utils/logger'
import { document } from './utils/globals'
import { assignableWindow, document } from './utils/globals'

// TODO: Add check for global config existing.
// Modify the whole "afterDecideResponse" function to be a method of the PostHog class

// 1. Option is to load the config endpoint (if __preview is enabled) and then if flags are needed call decide, and then call "afterDecideResponse" with the combined response :thinking:
// 2. Other option is to separate out the values so that we have a "config response" and a "flags response" separately...

// TODO: Fix WebExperiments to wait for flags instead of only decide
// TODO: Fix Surveys to do the same
// TODO: Background refresh of the config - every 5 minutes to match CDN cache - at least when active

export class Decide {
constructor(private readonly instance: PostHog) {
// don't need to wait for `decide` to return if flags were provided on initialisation
this.instance.decideEndpointWasHit = this.instance._hasBootstrappedFeatureFlags()
}

private _loadRemoteConfigJs(cb: (config?: RemoteConfig) => void): void {
if (assignableWindow.__PosthogExtensions__?.loadExternalDependency) {
assignableWindow.__PosthogExtensions__?.loadExternalDependency?.(this.instance, 'remote-config', () => {
return cb(assignableWindow._POSTHOG_CONFIG)
})
}
}

private _loadRemoteConfigJSON(cb: (config?: RemoteConfig) => void): void {
this.instance._send_request({
method: 'GET',
url: this.instance.requestRouter.endpointFor('assets', `/array/${this.instance.config.token}/config`),
callback: (response) => {
cb(response.json as RemoteConfig | undefined)
},
})
}

call(): void {
if (this.instance.config.__preview_remote_config) {
// Attempt 1 - use the pre-loaded config if it came as part of the token-specific array.js
if (assignableWindow._POSTHOG_CONFIG) {
this.onRemoteConfig(assignableWindow._POSTHOG_CONFIG)
return
}

// Attempt 2 - if we have the external deps loader then lets load the script version of the config that includes site apps
this._loadRemoteConfigJs((config) => {
if (!config) {
// Attempt 3 Load the config json instead of the script - we won't get site apps etc. but we will get the config
this._loadRemoteConfigJSON((config) => {
this.onRemoteConfig(config)
})
return
}

this.onRemoteConfig(config)
})

return
}

/*
Calls /decide endpoint to fetch options for autocapture, session recording, feature flags & compression.
*/
Expand Down Expand Up @@ -65,4 +116,22 @@ export class Decide {

this.instance._afterDecideResponse(response)
}

private onRemoteConfig(config?: RemoteConfig): void {
if (!config) {
logger.error('Failed to fetch remote config from PostHog.')
return
}
if (!(document && document.body)) {
logger.info('document not ready yet, trying again in 500 milliseconds...')
setTimeout(() => {
this.onRemoteConfig(config)
}, 500)
return
}

this.instance._onRemoteConfig(config)

// Additionally trigger loading of flags if necessary
}
}
4 changes: 4 additions & 0 deletions src/entrypoints/external-scripts-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ assignableWindow.__PosthogExtensions__.loadExternalDependency = (
): void => {
let scriptUrlToLoad = `/static/${kind}.js` + `?v=${posthog.version}`

if (kind === 'remote-config') {
scriptUrlToLoad = `/array/${posthog.config.token}/config.js`
}

if (kind === 'toolbar') {
// toolbar.js is served from the PostHog CDN, this has a TTL of 24 hours.
// the toolbar asset includes a rotating "token" that is valid for 5 minutes.
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/dead-clicks-autocapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DEAD_CLICKS_ENABLED_SERVER_SIDE } from '../constants'
import { isBoolean, isObject } from '../utils/type-utils'
import { assignableWindow, document, LazyLoadedDeadClicksAutocaptureInterface } from '../utils/globals'
import { logger } from '../utils/logger'
import { DeadClicksAutoCaptureConfig, DecideResponse } from '../types'
import { DeadClicksAutoCaptureConfig, RemoteConfig } from '../types'

const LOGGER_PREFIX = '[Dead Clicks]'

Expand Down Expand Up @@ -31,7 +31,7 @@ export class DeadClicksAutocapture {
this.startIfEnabled()
}

public afterDecideResponse(response: DecideResponse) {
public onRemoteConfig(response: RemoteConfig) {
if (this.instance.persistence) {
this.instance.persistence.register({
[DEAD_CLICKS_ENABLED_SERVER_SIDE]: response?.captureDeadClicks,
Expand Down
4 changes: 2 additions & 2 deletions src/extensions/exception-autocapture/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { assignableWindow, window } from '../../utils/globals'
import { PostHog } from '../../posthog-core'
import { DecideResponse, Properties } from '../../types'
import { Properties, RemoteConfig } from '../../types'

import { logger } from '../../utils/logger'
import { EXCEPTION_CAPTURE_ENABLED_SERVER_SIDE } from '../../constants'
Expand Down Expand Up @@ -86,7 +86,7 @@ export class ExceptionObserver {
this.unwrapUnhandledRejection?.()
}

afterDecideResponse(response: DecideResponse) {
onRemoteConfig(response: RemoteConfig) {
const autocaptureExceptionsResponse = response.autocaptureExceptions

// store this in-memory in case persistence is disabled
Expand Down
8 changes: 4 additions & 4 deletions src/extensions/replay/sessionrecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ import {
import { PostHog } from '../../posthog-core'
import {
CaptureResult,
DecideResponse,
FlagVariant,
NetworkRecordOptions,
NetworkRequest,
Properties,
RemoteConfig,
SessionRecordingUrlTrigger,
} from '../../types'
import {
Expand Down Expand Up @@ -617,8 +617,8 @@ export class SessionRecording {
})
}

afterDecideResponse(response: DecideResponse) {
this._persistDecideResponse(response)
onRemoteConfig(response: RemoteConfig) {
this._persistRemoteConfig(response)

this._linkedFlag = response.sessionRecording?.linkedFlag || null

Expand Down Expand Up @@ -671,7 +671,7 @@ export class SessionRecording {
}
}

private _persistDecideResponse(response: DecideResponse): void {
private _persistRemoteConfig(response: RemoteConfig): void {
if (this.instance.persistence) {
const persistence = this.instance.persistence

Expand Down
4 changes: 2 additions & 2 deletions src/extensions/web-vitals/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PostHog } from '../../posthog-core'
import { DecideResponse, SupportedWebVitalsMetrics } from '../../types'
import { RemoteConfig, SupportedWebVitalsMetrics } from '../../types'
import { logger } from '../../utils/logger'
import { isBoolean, isNullish, isNumber, isObject, isUndefined } from '../../utils/type-utils'
import { WEB_VITALS_ALLOWED_METRICS, WEB_VITALS_ENABLED_SERVER_SIDE } from '../../constants'
Expand Down Expand Up @@ -70,7 +70,7 @@ export class WebVitalsAutocapture {
}
}

public afterDecideResponse(response: DecideResponse) {
public onRemoteConfig(response: RemoteConfig) {
const webVitalsOptIn = isObject(response.capturePerformance) && !!response.capturePerformance.web_vitals

const allowedMetrics = isObject(response.capturePerformance)
Expand Down
4 changes: 2 additions & 2 deletions src/heatmaps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { includes, registerEvent } from './utils'
import RageClick from './extensions/rageclick'
import { DeadClickCandidate, DecideResponse, Properties } from './types'
import { DeadClickCandidate, Properties, RemoteConfig } from './types'
import { PostHog } from './posthog-core'

import { document, window } from './utils/globals'
Expand Down Expand Up @@ -99,7 +99,7 @@ export class Heatmaps {
}
}

public afterDecideResponse(response: DecideResponse) {
public onRemoteConfig(response: RemoteConfig) {
const optIn = !!response['heatmaps']

if (this.instance.persistence) {
Expand Down
25 changes: 17 additions & 8 deletions src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
Properties,
Property,
QueuedRequestOptions,
RemoteConfig,
RequestCallback,
SessionIdChangedCallback,
SnippetArrayItem,
Expand Down Expand Up @@ -567,15 +568,23 @@ export class PostHog {
: 'always',
})

this.siteApps?.afterDecideResponse(response)
this.sessionRecording?.afterDecideResponse(response)
this.autocapture?.afterDecideResponse(response)
this.heatmaps?.afterDecideResponse(response)
this.siteApps?.onRemoteConfig(response)
this.sessionRecording?.onRemoteConfig(response)
this.autocapture?.onRemoteConfig(response)
this.heatmaps?.onRemoteConfig(response)
this.experiments?.afterDecideResponse(response)
this.surveys?.afterDecideResponse(response)
this.webVitalsAutocapture?.afterDecideResponse(response)
this.exceptionObserver?.afterDecideResponse(response)
this.deadClicksAutocapture?.afterDecideResponse(response)
this.surveys?.onRemoteConfig(response)
this.webVitalsAutocapture?.onRemoteConfig(response)
this.exceptionObserver?.onRemoteConfig(response)
this.deadClicksAutocapture?.onRemoteConfig(response)
}

_onRemoteConfig(config: RemoteConfig) {
// TODO: check config. If "hasFlags" is anything other than false - load the flags from decide (later will be /flags)

if (config.hasFeatureFlags !== false) {
// Check explicitly for false - anything else means we there could be so lets load them
}
}

_loaded(): void {
Expand Down
4 changes: 2 additions & 2 deletions src/posthog-surveys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { isUrlMatchingRegex } from './utils/request-utils'
import { SurveyEventReceiver } from './utils/survey-event-receiver'
import { assignableWindow, document, window } from './utils/globals'
import { DecideResponse } from './types'
import { RemoteConfig } from './types'
import { logger } from './utils/logger'
import { isNullish } from './utils/type-utils'
import { getSurveySeenStorageKeys } from './extensions/surveys/surveys-utils'
Expand Down Expand Up @@ -69,7 +69,7 @@ export class PostHogSurveys {
this._surveyEventReceiver = null
}

afterDecideResponse(response: DecideResponse) {
onRemoteConfig(response: RemoteConfig) {
this._decideServerResponse = !!response['surveys']
this.loadIfEnabled()
}
Expand Down
4 changes: 2 additions & 2 deletions src/site-apps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PostHog } from './posthog-core'
import { CaptureResult, DecideResponse } from './types'
import { CaptureResult, RemoteConfig } from './types'
import { assignableWindow } from './utils/globals'
import { logger } from './utils/logger'
import { isArray } from './utils/type-utils'
Expand Down Expand Up @@ -74,7 +74,7 @@ export class SiteApps {
return globals
}

afterDecideResponse(response?: DecideResponse): void {
onRemoteConfig(response?: RemoteConfig): void {
if (assignableWindow._POSTHOG_SITE_APPS) {
// Loaded via new config so we have the apps preloaded
if (this.instance.config.opt_in_site_apps) {
Expand Down
18 changes: 14 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,12 @@ export interface PostHogConfig {
* whether to wrap fetch and add tracing headers to the request
* */
__add_tracing_headers?: boolean

/**
* PREVIEW - MAY CHANGE WITHOUT WARNING - DO NOT USE IN PRODUCTION
* whether to wrap fetch and add tracing headers to the request
* */
__preview_remote_config?: boolean
}

export interface OptInOutCapturingOptions {
Expand Down Expand Up @@ -480,11 +486,8 @@ export type SessionRecordingCanvasOptions = {
canvasQuality?: string | null
}

export interface DecideResponse {
export interface RemoteConfig {
supportedCompression: Compression[]
featureFlags: Record<string, string | boolean>
featureFlagPayloads: Record<string, JsonType>
errorsWhileComputingFlags: boolean
autocapture_opt_out?: boolean
/**
* originally capturePerformance was replay only and so boolean true
Expand Down Expand Up @@ -528,6 +531,13 @@ export interface DecideResponse {
heatmaps?: boolean
defaultIdentifiedOnly?: boolean
captureDeadClicks?: boolean
hasFeatureFlags?: boolean // Indicates if the team has any flags enabled (if not we don't need to load them)
}

export interface DecideResponse extends RemoteConfig {
featureFlags: Record<string, string | boolean>
featureFlagPayloads: Record<string, JsonType>
errorsWhileComputingFlags: boolean
}

export type FeatureFlagsCallback = (
Expand Down
5 changes: 3 additions & 2 deletions src/utils/globals.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ErrorProperties } from '../extensions/exception-autocapture/error-conversion'
import type { PostHog } from '../posthog-core'
import { SessionIdManager } from '../sessionid'
import { DeadClicksAutoCaptureConfig, ErrorEventArgs, ErrorMetadata, Properties } from '../types'
import { DeadClicksAutoCaptureConfig, ErrorEventArgs, ErrorMetadata, Properties, RemoteConfig } from '../types'

/*
* Global helpers to protect access to browser globals in a way that is safer for different targets
Expand All @@ -20,7 +20,7 @@ export type AssignableWindow = Window &
typeof globalThis &
Record<string, any> & {
__PosthogExtensions__?: PostHogExtensions
_POSTHOG_CONFIG?: Record<string, any> // TODO: Better typing
_POSTHOG_CONFIG?: RemoteConfig
_POSTHOG_SITE_APPS?: { token: string; load: (posthog: PostHog) => void }[]
}

Expand All @@ -37,6 +37,7 @@ export type PostHogExtensionKind =
| 'tracing-headers'
| 'surveys'
| 'dead-clicks-autocapture'
| 'remote-config'

export interface LazyLoadedDeadClicksAutocaptureInterface {
start: (observerTarget: Node) => void
Expand Down
4 changes: 1 addition & 3 deletions src/web-experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ export class WebExperiments {
this.applyFeatureFlagChanges(flags)
}

if (this.instance.onFeatureFlags) {
this.instance.onFeatureFlags(appFeatureFLags)
}
this.instance.onFeatureFlags(appFeatureFLags)
this._flagToExperiments = new Map<string, WebExperiment>()
}

Expand Down

0 comments on commit 57d6b77

Please sign in to comment.