@99ways/posthog-page-experiments 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +30 -4
- package/dist/index.d.ts +37 -0
- package/dist/index.js +1 -1
- package/dist/posthog-page-experiments.min.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented here. The project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-10-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- An assignment summary: every run logs one `console.info` line with the applied variant, its source (`posthog`, `url`, `fallback` or `ineligible`), the raw flag value, PostHog's evaluation reason, the device and distinct IDs, and a plain-language note when the default variant was shown for a specific cause, such as a timeout, an unknown variant or a browser without a PostHog device ID.
|
|
10
|
+
- A `log` option: `'summary'` (default), `'debug'` or `'silent'`.
|
|
11
|
+
- A `?experiment-debug` URL parameter that turns on debug logs for every experiment on the page, or `?experiment-debug=flag-a,flag-b` for the listed flags only.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- The package is no longer silent by default: each run logs its one-line assignment summary. Set `log: 'silent'` to restore the 0.1.0 output. `debug: true` still enables step-by-step diagnostics, as shorthand for `log: 'debug'`.
|
|
16
|
+
|
|
5
17
|
## [0.1.0] - 2026-09-17
|
|
6
18
|
|
|
7
19
|
### Added
|
|
@@ -14,4 +26,5 @@ All notable changes to this project will be documented here. The project follows
|
|
|
14
26
|
- ESM, browser/IIFE, and TypeScript declaration builds.
|
|
15
27
|
- Unit and real-browser tests using an actual `posthog-js` client.
|
|
16
28
|
|
|
29
|
+
[0.2.0]: https://github.com/99ways-io/posthog-page-experiments/releases/tag/v0.2.0
|
|
17
30
|
[0.1.0]: https://github.com/99ways-io/posthog-page-experiments/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -64,10 +64,10 @@ The default variant must be present in `variants`. An empty `control: []` keeps
|
|
|
64
64
|
|
|
65
65
|
### Direct browser script
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
The minified browser build is available from npm CDNs:
|
|
68
68
|
|
|
69
69
|
```html
|
|
70
|
-
<script src="https://cdn.jsdelivr.net/npm/@99ways/posthog-page-experiments@0.
|
|
70
|
+
<script src="https://cdn.jsdelivr.net/npm/@99ways/posthog-page-experiments@0.2.0/dist/posthog-page-experiments.min.js"></script>
|
|
71
71
|
<script>
|
|
72
72
|
window.runExperiment('pricing-page-headline', {
|
|
73
73
|
isEligible: () => document.querySelector('.pricing-hero') !== null,
|
|
@@ -123,6 +123,31 @@ https://example.com/pricing?pricing-page-headline=test
|
|
|
123
123
|
|
|
124
124
|
A configured URL variant takes precedence over PostHog, which makes review possible before a rollout exists. Eligibility still runs first. An unknown variant falls back to the configured default.
|
|
125
125
|
|
|
126
|
+
## See why a variant was shown
|
|
127
|
+
|
|
128
|
+
Each run logs one assignment summary to the browser console:
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
[Experiment:pricing-page-headline] Variant 'control' (from posthog). {
|
|
132
|
+
featureFlag: 'pricing-page-headline',
|
|
133
|
+
variant: 'control',
|
|
134
|
+
source: 'posthog',
|
|
135
|
+
flagValue: false,
|
|
136
|
+
reason: 'Out of rollout bound',
|
|
137
|
+
deviceId: null,
|
|
138
|
+
distinctId: 'anonymous-visitor-id',
|
|
139
|
+
note: "PostHog returned no variant, and this browser has no PostHog device ID. Flags that bucket by device cannot assign it, so 'control' is shown."
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
- `source` is `posthog`, `url` (a URL override), `fallback` (PostHog unavailable, timed out or failed) or `ineligible`.
|
|
144
|
+
- `reason`, `deviceId` and `distinctId` appear when the PostHog client exposes them. Recent `posthog-js` versions do.
|
|
145
|
+
- `note` explains plainly when the default variant was shown for a specific cause.
|
|
146
|
+
|
|
147
|
+
Reading these values does not record a feature-flag call.
|
|
148
|
+
|
|
149
|
+
For the step-by-step lifecycle, add `?experiment-debug` to the URL; `?experiment-debug=flag-a,flag-b` limits it to the listed flags. Alternatively, pass `log: 'debug'`. Use `log: 'silent'` to turn all output off.
|
|
150
|
+
|
|
126
151
|
## Choose the right tool
|
|
127
152
|
|
|
128
153
|
Use this package when:
|
|
@@ -152,7 +177,8 @@ The promise rejects if eligibility or a variant handler throws.
|
|
|
152
177
|
| `isEligible` | `() => boolean \| PromiseLike<boolean>` | `() => true` | Runs before the flag is evaluated. |
|
|
153
178
|
| `posthog` | `PostHogClient` | `window.posthog` | Explicit initialized client for module use. |
|
|
154
179
|
| `featureFlagTimeoutMs` | `number` | `4000` | Maximum wait for PostHog and its first feature-flag notification. |
|
|
155
|
-
| `
|
|
180
|
+
| `log` | `'summary' \| 'debug' \| 'silent'` | `'summary'` | Console output: one assignment summary, plus step-by-step diagnostics with `debug`, or nothing. |
|
|
181
|
+
| `debug` | `boolean` | `false` | Shorthand for `log: 'debug'`. Ignored when `log` is set. |
|
|
156
182
|
|
|
157
183
|
Boolean flags map `true` to `test` and `false` to the default variant. String-valued flags map directly to the matching configured variant.
|
|
158
184
|
|
|
@@ -218,7 +244,7 @@ The package does not inject a global anti-flicker snippet because a generic page
|
|
|
218
244
|
|
|
219
245
|
The distributed JavaScript targets modern evergreen browsers and uses standard browser APIs including `Promise`, `URLSearchParams`, and `querySelectorAll`.
|
|
220
246
|
|
|
221
|
-
There is no runtime dependency on `posthog-js`. Compatibility is structural: the client must expose `__loaded`, `onFeatureFlags()`, and `getFeatureFlag()`. CI tests the current release against `posthog-js` 1.433.9 in Chromium.
|
|
247
|
+
There is no runtime dependency on `posthog-js`. Compatibility is structural: the client must expose `__loaded`, `onFeatureFlags()`, and `getFeatureFlag()`. The assignment summary also reads `featureFlags.getFeatureFlagDetails()`, `get_property()`, and `get_distinct_id()` when they exist. CI tests the current release against `posthog-js` 1.433.9 in Chromium.
|
|
222
248
|
|
|
223
249
|
The project follows Semantic Versioning. Before `1.0.0`, breaking public-API changes may ship in a minor release; they will be called out in the changelog. The internal experiment class is not part of the public contract.
|
|
224
250
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
export type Awaitable<T> = T | PromiseLike<T>;
|
|
2
|
+
export interface PostHogFeatureFlagDetails {
|
|
3
|
+
reason?: {
|
|
4
|
+
code?: string | null;
|
|
5
|
+
description?: string | null;
|
|
6
|
+
} | null;
|
|
7
|
+
}
|
|
2
8
|
export interface PostHogClient {
|
|
3
9
|
__loaded?: boolean;
|
|
4
10
|
getFeatureFlag(featureFlag: string): boolean | string | null | undefined;
|
|
5
11
|
onFeatureFlags(callback: () => void): (() => void) | void;
|
|
12
|
+
/** Optional. Read for the assignment summary when the client provides it. */
|
|
13
|
+
featureFlags?: {
|
|
14
|
+
getFeatureFlagDetails?(featureFlag: string): PostHogFeatureFlagDetails | null | undefined;
|
|
15
|
+
};
|
|
16
|
+
/** Optional. Read for the assignment summary when the client provides it. */
|
|
17
|
+
get_property?(property: string): unknown;
|
|
18
|
+
/** Optional. Read for the assignment summary when the client provides it. */
|
|
19
|
+
get_distinct_id?(): string;
|
|
6
20
|
}
|
|
7
21
|
export type StyleUpdates = Record<string, string | number | null | undefined>;
|
|
8
22
|
export interface ElementUpdates {
|
|
@@ -17,11 +31,34 @@ export interface ElementChange {
|
|
|
17
31
|
}
|
|
18
32
|
export type VariantStep = ElementChange | (() => unknown);
|
|
19
33
|
export type VariantHandler = VariantStep[] | (() => unknown);
|
|
34
|
+
/**
|
|
35
|
+
* `summary` logs one assignment line per run, `debug` adds step-by-step diagnostics,
|
|
36
|
+
* and `silent` logs nothing.
|
|
37
|
+
*/
|
|
38
|
+
export type ExperimentLogLevel = 'silent' | 'summary' | 'debug';
|
|
39
|
+
export type AssignmentSource = 'url' | 'posthog' | 'fallback' | 'ineligible';
|
|
40
|
+
/** What the assignment summary reports for one run. */
|
|
41
|
+
export interface ExperimentAssignment {
|
|
42
|
+
featureFlag: string;
|
|
43
|
+
/** The applied variant, or `null` when the visitor is ineligible. */
|
|
44
|
+
variant: string | null;
|
|
45
|
+
source: AssignmentSource;
|
|
46
|
+
/** The raw value PostHog returned. Present only when the source is `posthog`. */
|
|
47
|
+
flagValue?: boolean | string | null;
|
|
48
|
+
/** PostHog's evaluation reason, such as "Matched condition set 1", when the client exposes it. */
|
|
49
|
+
reason?: string;
|
|
50
|
+
deviceId?: string | null;
|
|
51
|
+
distinctId?: string | null;
|
|
52
|
+
/** A plain-language explanation when the default variant was shown for a specific cause. */
|
|
53
|
+
note?: string;
|
|
54
|
+
}
|
|
20
55
|
export interface ExperimentOptions {
|
|
21
56
|
variants: Record<string, VariantHandler>;
|
|
22
57
|
defaultVariant?: string;
|
|
23
58
|
isEligible?: () => Awaitable<boolean>;
|
|
24
59
|
posthog?: PostHogClient;
|
|
60
|
+
log?: ExperimentLogLevel;
|
|
61
|
+
/** Shorthand for `log: 'debug'`. Ignored when `log` is set. */
|
|
25
62
|
debug?: boolean;
|
|
26
63
|
featureFlagTimeoutMs?: number;
|
|
27
64
|
}
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
class m{featureFlag;variants;defaultVariant;isEligible;explicitPostHog;
|
|
1
|
+
class m{featureFlag;variants;defaultVariant;isEligible;explicitPostHog;logLevel;featureFlagTimeoutMs;runPromise=null;constructor(e,t){this.featureFlag=e;if(!e.trim())throw TypeError("featureFlag must be a non-empty string.");if(this.defaultVariant=t.defaultVariant??"control",this.variants=new Map(Object.entries(t.variants)),this.isEligible=t.isEligible??(()=>!0),this.explicitPostHog=t.posthog,this.logLevel=t.log??(t.debug?"debug":"summary"),this.featureFlagTimeoutMs=t.featureFlagTimeoutMs??4000,!["silent","summary","debug"].includes(this.logLevel))throw TypeError("log must be 'silent', 'summary', or 'debug'.");if(!Number.isFinite(this.featureFlagTimeoutMs)||this.featureFlagTimeoutMs<0)throw RangeError("featureFlagTimeoutMs must be a finite number greater than or equal to 0.");if(!this.defaultVariant.trim())throw TypeError("defaultVariant must be a non-empty string.");if(!this.variants.has(this.defaultVariant))throw TypeError(`variants must include the default variant '${this.defaultVariant}'.`)}run(){if(!this.runPromise)this.runPromise=this.runOnce();return this.runPromise}async runOnce(){if(typeof window>"u"||typeof document>"u")return this.log("Browser globals are unavailable; skipping the experiment."),null;if(this.isDebugRequestedByUrl())this.logLevel="debug";if(await this.waitForDOMContentLoaded(),this.log("Checking eligibility before evaluating the feature flag."),!await this.isEligible())return this.log("Visitor is ineligible; skipping feature-flag evaluation and variant application."),this.summarize({featureFlag:this.featureFlag,variant:null,source:"ineligible",note:"The visitor is not eligible, so the feature flag was not evaluated."}),null;let e=await this.resolveVariant(),t=this.getConfiguredVariantOrDefault(e.value);return this.log(`Activated variant '${t}'.`),this.summarize(this.describeAssignment(e,t)),this.applyVariant(t),t}getVariantFromQueryParam(){return new URLSearchParams(window.location.search).get(this.featureFlag)}isDebugRequestedByUrl(){let e=new URLSearchParams(window.location.search).get("experiment-debug");if(e===null)return!1;let t=e.split(",").map((i)=>i.trim()).filter(Boolean);return t.length===0||t.includes("1")||t.includes("true")||t.includes(this.featureFlag)}getPostHog(){let e=this.explicitPostHog??window.posthog;if(e&&e.__loaded===!0&&typeof e.onFeatureFlags==="function"&&typeof e.getFeatureFlag==="function")return e;return}resolveVariant(){let e=this.getVariantFromQueryParam();if(e)return this.log(`Using query-string override variant '${e}'.`),Promise.resolve({value:e,source:"url"});return this.log("Waiting for initialized PostHog and its first feature-flag notification."),new Promise((t)=>{let i=!1,r,n=!1,a,s,o=()=>{if(!r)return!1;let l=r;r=void 0;try{l()}catch(u){this.warn(`Could not unsubscribe from feature flag '${this.featureFlag}'.`,u)}return!0},d=()=>{if(a!==void 0)clearInterval(a);if(s!==void 0)clearTimeout(s);if(!o())n=!0},f=(l)=>{if(i)return;i=!0,d(),t(l)},p=()=>{if(i)return!0;let l=this.getPostHog();if(!l)return!1;if(a!==void 0)clearInterval(a),a=void 0;try{this.log("PostHog is initialized; subscribing to feature flags.");let u=l.onFeatureFlags(()=>{try{let g=this.getPostHog();if(!g)throw Error("The initialized PostHog instance is no longer available.");let c=g.getFeatureFlag(this.featureFlag),h=this.mapFeatureFlagValue(c);this.log(`PostHog resolved variant '${h}' from raw value ${JSON.stringify(c)}.`),f({value:h,source:"posthog",flagValue:c??null,client:g})}catch(g){this.warn(`Could not read feature flag '${this.featureFlag}'. Applying '${this.defaultVariant}'.`,g),f({value:this.defaultVariant,source:"fallback",fallback:"error"})}});if(typeof u==="function")r=u;if(n)n=!1,o();return!0}catch(u){return this.warn(`Could not subscribe to feature flag '${this.featureFlag}'. Applying '${this.defaultVariant}'.`,u),f({value:this.defaultVariant,source:"fallback",fallback:"error"}),!0}};if(s=setTimeout(()=>{this.warn(`Feature flag '${this.featureFlag}' was not resolved within ${this.featureFlagTimeoutMs}ms. Applying '${this.defaultVariant}'.`),f({value:this.defaultVariant,source:"fallback",fallback:"timeout"})},this.featureFlagTimeoutMs),!p())a=setInterval(p,50)})}mapFeatureFlagValue(e){if(e===!0)return"test";if(e===!1||e===null||e===void 0||e==="")return this.defaultVariant;return e}getConfiguredVariantOrDefault(e){if(this.variants.has(e))return e;return this.warn(`Variant '${e}' is not configured for '${this.featureFlag}'. Applying '${this.defaultVariant}'.`),this.defaultVariant}describeAssignment(e,t){let i={featureFlag:this.featureFlag,variant:t,source:e.source},r=`'${t}' is shown`;if(e.source==="url")return i.note=t===e.value?"Variant forced by the URL parameter; PostHog was not called.":`URL variant '${e.value}' is not configured, so ${r}.`,i;if(e.source==="fallback")return i.note=e.fallback==="timeout"?`PostHog did not return feature flags within ${this.featureFlagTimeoutMs}ms, so ${r}.`:`The feature flag could not be read, so ${r}.`,i;let n=e.client;i.flagValue=e.flagValue??null;let a=this.readSafely(()=>{let o=n?.featureFlags?.getFeatureFlagDetails?.(this.featureFlag);return o?.reason?.description||o?.reason?.code||void 0});if(a)i.reason=a;if(typeof n?.get_property==="function")i.deviceId=this.readSafely(()=>this.asText(n.get_property?.("$device_id")));if(typeof n?.get_distinct_id==="function")i.distinctId=this.readSafely(()=>this.asText(n.get_distinct_id?.()));let s=i.flagValue;if(s===!1||s===null||s==="")i.note=i.deviceId===null?`PostHog returned no variant, and this browser has no PostHog device ID. Flags that bucket by device cannot assign it, so ${r}.`:`PostHog returned no variant for this visitor, so ${r}.`;else if(t!==e.value)i.note=`Flag value '${e.value}' is not a configured variant, so ${r}.`;return i}readSafely(e){try{return e()}catch{return}}asText(e){return typeof e==="string"&&e?e:null}summarize(e){if(this.logLevel==="silent")return;let t=e.source==="ineligible"?"not applied":`from ${e.source}`;console.info(`[Experiment:${this.featureFlag}] Variant ${e.variant===null?"none":`'${e.variant}'`} (${t}).`,e)}waitForDOMContentLoaded(){if(document.readyState!=="loading")return this.log("DOM is already ready; skipping the DOMContentLoaded wait."),Promise.resolve();return this.log("Waiting for DOMContentLoaded."),new Promise((e)=>{document.addEventListener("DOMContentLoaded",()=>{this.log("DOMContentLoaded received."),e()},{once:!0})})}applyVariant(e){let t=this.variants.get(e);if(!t)return;this.log(`Applying variant '${e}'.`),this.applyHandler(t,e),this.log(`Finished applying variant '${e}'.`)}applyHandler(e,t){if(typeof e==="function"){e();return}e.forEach((i)=>{if(typeof i==="function"){i();return}this.applyUpdates(i.selector,i.updates,t)})}applyUpdates(e,t,i){let r=document.querySelectorAll(e);if(!r.length){this.warn(`No elements found for selector '${e}'.`);return}this.log(`Selector '${e}' matched ${r.length} element(s).`),r.forEach((n)=>{if(t.style)Object.entries(t.style).forEach(([a,s])=>{if(s===null||s===void 0)return;let o=a.startsWith("--")?a:a.replace(/[A-Z]/g,(d)=>`-${d.toLowerCase()}`);n.style.setProperty(o,String(s),"important")});if(t.innerText!==void 0)n.innerText=t.innerText;if(t.innerHTML!==void 0)n.innerHTML=t.innerHTML;t.callback?.(n,i)})}log(e,...t){if(this.logLevel==="debug")console.log(`[Experiment:${this.featureFlag}] ${e}`,...t)}warn(e,...t){if(this.logLevel==="debug")console.warn(`[Experiment:${this.featureFlag}] ${e}`,...t)}}function v(e,t){return new m(e,t).run()}export{v as runExperiment};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
(()=>{class m{featureFlag;variants;defaultVariant;isEligible;explicitPostHog;
|
|
1
|
+
(()=>{class m{featureFlag;variants;defaultVariant;isEligible;explicitPostHog;logLevel;featureFlagTimeoutMs;runPromise=null;constructor(e,t){this.featureFlag=e;if(!e.trim())throw TypeError("featureFlag must be a non-empty string.");if(this.defaultVariant=t.defaultVariant??"control",this.variants=new Map(Object.entries(t.variants)),this.isEligible=t.isEligible??(()=>!0),this.explicitPostHog=t.posthog,this.logLevel=t.log??(t.debug?"debug":"summary"),this.featureFlagTimeoutMs=t.featureFlagTimeoutMs??4000,!["silent","summary","debug"].includes(this.logLevel))throw TypeError("log must be 'silent', 'summary', or 'debug'.");if(!Number.isFinite(this.featureFlagTimeoutMs)||this.featureFlagTimeoutMs<0)throw RangeError("featureFlagTimeoutMs must be a finite number greater than or equal to 0.");if(!this.defaultVariant.trim())throw TypeError("defaultVariant must be a non-empty string.");if(!this.variants.has(this.defaultVariant))throw TypeError(`variants must include the default variant '${this.defaultVariant}'.`)}run(){if(!this.runPromise)this.runPromise=this.runOnce();return this.runPromise}async runOnce(){if(typeof window>"u"||typeof document>"u")return this.log("Browser globals are unavailable; skipping the experiment."),null;if(this.isDebugRequestedByUrl())this.logLevel="debug";if(await this.waitForDOMContentLoaded(),this.log("Checking eligibility before evaluating the feature flag."),!await this.isEligible())return this.log("Visitor is ineligible; skipping feature-flag evaluation and variant application."),this.summarize({featureFlag:this.featureFlag,variant:null,source:"ineligible",note:"The visitor is not eligible, so the feature flag was not evaluated."}),null;let e=await this.resolveVariant(),t=this.getConfiguredVariantOrDefault(e.value);return this.log(`Activated variant '${t}'.`),this.summarize(this.describeAssignment(e,t)),this.applyVariant(t),t}getVariantFromQueryParam(){return new URLSearchParams(window.location.search).get(this.featureFlag)}isDebugRequestedByUrl(){let e=new URLSearchParams(window.location.search).get("experiment-debug");if(e===null)return!1;let t=e.split(",").map((i)=>i.trim()).filter(Boolean);return t.length===0||t.includes("1")||t.includes("true")||t.includes(this.featureFlag)}getPostHog(){let e=this.explicitPostHog??window.posthog;if(e&&e.__loaded===!0&&typeof e.onFeatureFlags==="function"&&typeof e.getFeatureFlag==="function")return e;return}resolveVariant(){let e=this.getVariantFromQueryParam();if(e)return this.log(`Using query-string override variant '${e}'.`),Promise.resolve({value:e,source:"url"});return this.log("Waiting for initialized PostHog and its first feature-flag notification."),new Promise((t)=>{let i=!1,n,r=!1,a,s,o=()=>{if(!n)return!1;let l=n;n=void 0;try{l()}catch(u){this.warn(`Could not unsubscribe from feature flag '${this.featureFlag}'.`,u)}return!0},d=()=>{if(a!==void 0)clearInterval(a);if(s!==void 0)clearTimeout(s);if(!o())r=!0},f=(l)=>{if(i)return;i=!0,d(),t(l)},p=()=>{if(i)return!0;let l=this.getPostHog();if(!l)return!1;if(a!==void 0)clearInterval(a),a=void 0;try{this.log("PostHog is initialized; subscribing to feature flags.");let u=l.onFeatureFlags(()=>{try{let g=this.getPostHog();if(!g)throw Error("The initialized PostHog instance is no longer available.");let c=g.getFeatureFlag(this.featureFlag),h=this.mapFeatureFlagValue(c);this.log(`PostHog resolved variant '${h}' from raw value ${JSON.stringify(c)}.`),f({value:h,source:"posthog",flagValue:c??null,client:g})}catch(g){this.warn(`Could not read feature flag '${this.featureFlag}'. Applying '${this.defaultVariant}'.`,g),f({value:this.defaultVariant,source:"fallback",fallback:"error"})}});if(typeof u==="function")n=u;if(r)r=!1,o();return!0}catch(u){return this.warn(`Could not subscribe to feature flag '${this.featureFlag}'. Applying '${this.defaultVariant}'.`,u),f({value:this.defaultVariant,source:"fallback",fallback:"error"}),!0}};if(s=setTimeout(()=>{this.warn(`Feature flag '${this.featureFlag}' was not resolved within ${this.featureFlagTimeoutMs}ms. Applying '${this.defaultVariant}'.`),f({value:this.defaultVariant,source:"fallback",fallback:"timeout"})},this.featureFlagTimeoutMs),!p())a=setInterval(p,50)})}mapFeatureFlagValue(e){if(e===!0)return"test";if(e===!1||e===null||e===void 0||e==="")return this.defaultVariant;return e}getConfiguredVariantOrDefault(e){if(this.variants.has(e))return e;return this.warn(`Variant '${e}' is not configured for '${this.featureFlag}'. Applying '${this.defaultVariant}'.`),this.defaultVariant}describeAssignment(e,t){let i={featureFlag:this.featureFlag,variant:t,source:e.source},n=`'${t}' is shown`;if(e.source==="url")return i.note=t===e.value?"Variant forced by the URL parameter; PostHog was not called.":`URL variant '${e.value}' is not configured, so ${n}.`,i;if(e.source==="fallback")return i.note=e.fallback==="timeout"?`PostHog did not return feature flags within ${this.featureFlagTimeoutMs}ms, so ${n}.`:`The feature flag could not be read, so ${n}.`,i;let r=e.client;i.flagValue=e.flagValue??null;let a=this.readSafely(()=>{let o=r?.featureFlags?.getFeatureFlagDetails?.(this.featureFlag);return o?.reason?.description||o?.reason?.code||void 0});if(a)i.reason=a;if(typeof r?.get_property==="function")i.deviceId=this.readSafely(()=>this.asText(r.get_property?.("$device_id")));if(typeof r?.get_distinct_id==="function")i.distinctId=this.readSafely(()=>this.asText(r.get_distinct_id?.()));let s=i.flagValue;if(s===!1||s===null||s==="")i.note=i.deviceId===null?`PostHog returned no variant, and this browser has no PostHog device ID. Flags that bucket by device cannot assign it, so ${n}.`:`PostHog returned no variant for this visitor, so ${n}.`;else if(t!==e.value)i.note=`Flag value '${e.value}' is not a configured variant, so ${n}.`;return i}readSafely(e){try{return e()}catch{return}}asText(e){return typeof e==="string"&&e?e:null}summarize(e){if(this.logLevel==="silent")return;let t=e.source==="ineligible"?"not applied":`from ${e.source}`;console.info(`[Experiment:${this.featureFlag}] Variant ${e.variant===null?"none":`'${e.variant}'`} (${t}).`,e)}waitForDOMContentLoaded(){if(document.readyState!=="loading")return this.log("DOM is already ready; skipping the DOMContentLoaded wait."),Promise.resolve();return this.log("Waiting for DOMContentLoaded."),new Promise((e)=>{document.addEventListener("DOMContentLoaded",()=>{this.log("DOMContentLoaded received."),e()},{once:!0})})}applyVariant(e){let t=this.variants.get(e);if(!t)return;this.log(`Applying variant '${e}'.`),this.applyHandler(t,e),this.log(`Finished applying variant '${e}'.`)}applyHandler(e,t){if(typeof e==="function"){e();return}e.forEach((i)=>{if(typeof i==="function"){i();return}this.applyUpdates(i.selector,i.updates,t)})}applyUpdates(e,t,i){let n=document.querySelectorAll(e);if(!n.length){this.warn(`No elements found for selector '${e}'.`);return}this.log(`Selector '${e}' matched ${n.length} element(s).`),n.forEach((r)=>{if(t.style)Object.entries(t.style).forEach(([a,s])=>{if(s===null||s===void 0)return;let o=a.startsWith("--")?a:a.replace(/[A-Z]/g,(d)=>`-${d.toLowerCase()}`);r.style.setProperty(o,String(s),"important")});if(t.innerText!==void 0)r.innerText=t.innerText;if(t.innerHTML!==void 0)r.innerHTML=t.innerHTML;t.callback?.(r,i)})}log(e,...t){if(this.logLevel==="debug")console.log(`[Experiment:${this.featureFlag}] ${e}`,...t)}warn(e,...t){if(this.logLevel==="debug")console.warn(`[Experiment:${this.featureFlag}] ${e}`,...t)}}function v(e,t){return new m(e,t).run()}if(typeof window<"u")window.runExperiment=v;})();
|
package/package.json
CHANGED