@obelism/improve-sdk 3.1.1 → 3.3.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/README.md CHANGED
@@ -21,6 +21,7 @@ While similar they both work slightly different but are based on the same base.
21
21
  ```ts
22
22
  constructor({
23
23
  organizationId: string
24
+ spaceId?: string
24
25
  environment: 'develop' | 'staging' | 'production'
25
26
  config?: Configuration
26
27
  fetchTimeout?: number
@@ -29,6 +30,7 @@ constructor({
29
30
  ```
30
31
 
31
32
  - **organizationId** - Unique identifier of your Organization, available from your dashboard
33
+ - **spaceId** - (optional) The space (`space_…`) to work in: one site or project of your Organization, with its own tests, flags, audiences, API tokens and allowed origins. Copy it from Settings → Implementation. Without it, the Organization's first space
32
34
  - **environment** - Application environment, can be one of three values
33
35
  - **config** - (optional) Configuration file, this can be either fetched or provided on initialization
34
36
  - **fetchTimeout** - (optional) When fetching the config after what amount of ms should it abort, default; 3000ms
@@ -139,6 +141,23 @@ getTestValue = (testSlug: string, visitorId: string, userAgent: string) =>
139
141
 
140
142
  Based on the slug, visitorId and userAgent this method gives you back what version of the AB test this visitor should get. If no config is loaded yet or no AB test with the given slug is found it returns `null`. When the userAgent of the visitor doesn't match the audience of the Test it will always return the control value. If the visitor does match the audience it will then check the allocation. If that's lower than 100% it will based on randomization check if this visitor should get the AB test. After that it will receive a random version of the test based on the randomization using the odds setup in the split.
141
143
 
144
+ ### Without a stored token
145
+
146
+ On Vercel, or in GitHub Actions, the server SDK can send the identity token its host gives it instead of an API token: nothing to store or rotate. Connect the deployment first, under **Connected deployments** on the space's page on www.obelism.studio, with Improve on for the environments it runs in (each maps to one environment of the space, which has to be the one the client asks for), then pass a token source:
147
+
148
+ ```ts
149
+ import { ImproveServerSDK } from '@obelism/improve-sdk/server'
150
+ import { vercelOidc } from '@obelism/improve-sdk/oidc' // needs @vercel/oidc installed
151
+
152
+ const improve = new ImproveServerSDK({
153
+ organizationId: 'org_…',
154
+ environment: 'production',
155
+ token: vercelOidc(), // or githubOidc() in a workflow with `permissions: id-token: write`
156
+ })
157
+ ```
158
+
159
+ `token` takes any function that gives a token per request. The tokens are requested for `https://improve.obelism.studio`; for events sent straight to numbers, use `vercelOidc({ audience: 'https://numbers.obelism.studio' })`.
160
+
142
161
  ## Client
143
162
 
144
163
  ### setupVisitor
@@ -0,0 +1,2 @@
1
+ var e=require("ua-parser-js");let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],r=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],i=["mac os","ios","android","windows","unix"],n=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],s=["wearable","mobile","tablet"],o="visi",a=e=>new Promise(t=>setTimeout(t,e)),u={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class l extends Error{constructor(e,t){super(u[e]),this.name="ImproveFetchError",this.reason=e,this.status=t?.status,this.retryAfterMs=t?.retryAfterMs,t&&"cause"in t&&(this.cause=t.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let h=e=>429===e?"rate-limited":401===e||403===e?"unauthorized":e>=500?"server":"client",c="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",f=e=>{if("object"!=typeof e||null===e||Array.isArray(e))throw new l("invalid-response");return{name:"string"==typeof e.name?e.name:"",version:"number"==typeof e.version?e.version:0,tests:e.tests??{},flags:e.flags??{},audience:e.audience??{}}},d=(e,t=Date.now())=>{if(!e)return;let r=Number(e);if(Number.isFinite(r))return Math.max(0,Math.round(1e3*r));let i=Date.parse(e);if(!Number.isNaN(i))return Math.max(0,i-t)},g=(e,t,r)=>Math.round(Math.random()*Math.min(r,t*2**e)),m=(e=3e3,t,r)=>{let i=new AbortController;return Promise.race([fetch(t,{...r,signal:i.signal}),(async(e=1e3,t)=>(await a(e),t?.abort(),null))(e,i)])};exports.ANALYTICS_PATH="/api/log",exports.BaseImproveSDK=class{#e;#t;constructor({organizationId:e,spaceId:t,environment:r,state:i,config:n,fetchTimeout:s,baseUrl:u,configRetries:w,configRevalidate:p,disableWarnings:v}){this.environment="develop",this.#e=null,this.#t=!1,this.config=null,this._fetchConfig=async e=>{if(this.config)return;if(!this.#e)throw Error("No config fetch setup provided");let{url:t,timeout:r,retries:i,revalidate:n}=this.#e,s={...e};"number"==typeof n&&(s.next={...e?.next,revalidate:n});let o=new l("network");for(let e=0;e<=i;e++){let i;e>0&&await a(o.retryAfterMs??g(e-1,300,3e3));try{i=await m(r,t,s)}catch(e){o=new l(e instanceof Error&&"AbortError"===e.name?"timeout":"network",{cause:e});continue}if(!i){o=new l("timeout");continue}if(i.ok){try{this.config=f(await i.json())}catch(e){o=e instanceof l?e:new l("invalid-response",{cause:e});continue}return this.config}if(!(o=new l(h(i.status),{status:i.status,retryAfterMs:d(i.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=e=>{this.config=e},this.generateVisitorId=()=>[o,(function(e=5){return e&&"number"==typeof e?Array(e).fill("").reduce(e=>e+=c.charAt(Math.floor(Math.random()*c.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=e=>{this.#t||"u">typeof console&&console.warn(`[Improve] ${e}`)},this.validateTestValue=(e,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let r=this.config.tests[e];if(!r)throw Error(`No config found for ${e}`);return!!r.options.find(e=>e.slug===t)},this.validateVisitorId=e=>{let t=e.split("_");if(2!==t.length)return!1;let[r,i]=t;return r===o&&26===i.length},this.organizationId=e,this.spaceId=t,this.environment=r,this.state=i,this._baseUrl=u||"https://improve.obelism.studio",this.#t=v??!1,n?this.config=n:this.#e={url:[`${this._baseUrl}/config`,this.spaceId||this.organizationId,this.environment,this.state||"active"].join("/"),timeout:s||3e3,retries:w??2,revalidate:p}}},exports.getRandomTestValue=e=>{let[t]=e;if(!t)return null;if(1===e.length)return t.slug;let r=Math.random()*e.reduce((e,{split:t})=>e+t,0);return(e.find(({split:e})=>(r-=e)<=0)||t).slug},exports.getVisitorMatchesAudience=(e,t)=>!e||Object.entries(e).every(([e,r])=>t[e]===r),exports.parseUserAgent=o=>{var a;if(!o||"string"!=typeof o)return null;let u=new e.UAParser(o).getResult(),l=(a=u.device.type)&&t.includes(a)?a:"desktop";return{pointer:s.includes(l)?"coarse":"fine",device:l,browser:((e="")=>{if(!e)return"other";let t=e.toLowerCase(),i=r.find(e=>t.includes(e));return i||(n.includes(t)?"social":"other")})(u.browser.name),os:((e="")=>{if(!e)return"unix";let t=e.toLowerCase(),r=i.find(e=>t.includes(e));return r||"unix"})(u.os.name)}};
2
+ //# sourceMappingURL=base-8q51gsqe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-8q51gsqe.js","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/config/constants.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/timeoutFetch.ts","../src/utils/getTimeoutError.ts","../src/config/urls.ts","../src/base.ts","../src/utils/getRandomTestValue.ts","../src/utils/getVisitorMatchesAudience.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Location — coarse IP-geolocation (ISO 3166-1 alpha-2 country code). Values\n\t//? aren't enumerated here (any country code is valid); the key exists so\n\t//? audiences can target `country`. Resolved server-side (SSR/middleware) from\n\t//? the request, since the browser can't determine it from the user agent.\n\tcountry: [] as ReadonlyArray<string>,\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n\t// Coarse geo, not derived from the user agent — merged in server-side\n\t// (SSR/middleware) so audiences can target by country. Absent client-side.\n\tcountry?: string\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number | undefined\n\tretryAfterMs?: number | undefined\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: {\n\t\t\tstatus?: number | undefined\n\t\t\tretryAfterMs?: number | undefined\n\t\t\tcause?: unknown\n\t\t},\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\n// Events go to Obelism numbers (the numbers SDK's default endpoint); only a\n// custom `baseUrl` sends them to its own /api/log.\nexport const ANALYTICS_PATH = `/api/log`\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number | undefined\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tspaceId: string | undefined\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState | undefined\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tspaceId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.spaceId = spaceId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\t// The space's datafile; the organization's id means its first\n\t\t\t\t\t// space.\n\t\t\t\t\tthis.spaceId || this.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tconst [first] = options\n\tif (!first) return null\n\tif (options.length === 1) return first.slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || first\n\n\treturn match.slug\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","VISITOR_ID_PREFIX","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","options","name","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","audience","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","Math","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","random","min","timeoutFetch","url","controller","AbortController","race","fetch","signal","getTimeoutError","abort","organizationId","spaceId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","reduce","acc","charAt","floor","length","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","find","option","slug","validateVisitorId","possibleVisitorId","visitorIdParts","split","key","value","_baseUrl","first","match","visitorParams","Object","entries","every","paramKey","paramValue","userAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{UAParser as e}from"ua-parser-js";let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],r=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],i=["mac os","ios","android","windows","unix"],n=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],s=["wearable","mobile","tablet"],o=o=>{var a;if(!o||"string"!=typeof o)return null;let u=new e(o).getResult(),l=(a=u.device.type)&&t.includes(a)?a:"desktop";return{pointer:s.includes(l)?"coarse":"fine",device:l,browser:((e="")=>{if(!e)return"other";let t=e.toLowerCase(),i=r.find(e=>t.includes(e));return i||(n.includes(t)?"social":"other")})(u.browser.name),os:((e="")=>{if(!e)return"unix";let t=e.toLowerCase(),r=i.find(e=>t.includes(e));return r||"unix"})(u.os.name)}},a=(e,t)=>!e||Object.entries(e).every(([e,r])=>t[e]===r),u=e=>{let[t]=e;if(!t)return null;if(1===e.length)return t.slug;let r=Math.random()*e.reduce((e,{split:t})=>e+t,0);return(e.find(({split:e})=>(r-=e)<=0)||t).slug},l="visi",h="/api/log",c=e=>new Promise(t=>setTimeout(t,e)),f={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class d extends Error{constructor(e,t){super(f[e]),this.name="ImproveFetchError",this.reason=e,this.status=t?.status,this.retryAfterMs=t?.retryAfterMs,t&&"cause"in t&&(this.cause=t.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let g=e=>429===e?"rate-limited":401===e||403===e?"unauthorized":e>=500?"server":"client",m="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",w=e=>{if("object"!=typeof e||null===e||Array.isArray(e))throw new d("invalid-response");return{name:"string"==typeof e.name?e.name:"",version:"number"==typeof e.version?e.version:0,tests:e.tests??{},flags:e.flags??{},audience:e.audience??{}}},p=(e,t=Date.now())=>{if(!e)return;let r=Number(e);if(Number.isFinite(r))return Math.max(0,Math.round(1e3*r));let i=Date.parse(e);if(!Number.isNaN(i))return Math.max(0,i-t)},v=(e,t,r)=>Math.round(Math.random()*Math.min(r,t*2**e)),b=async(e=1e3,t)=>(await c(e),t?.abort(),null),y=(e=3e3,t,r)=>{let i=new AbortController;return Promise.race([fetch(t,{...r,signal:i.signal}),b(e,i)])};class C{#e;#t;constructor({organizationId:e,environment:t,state:r,config:i,fetchTimeout:n,baseUrl:s,configRetries:o,configRevalidate:a,disableWarnings:u}){this.environment="develop",this.#e=null,this.#t=!1,this.config=null,this._fetchConfig=async e=>{if(this.config)return;if(!this.#e)throw Error("No config fetch setup provided");let{url:t,timeout:r,retries:i,revalidate:n}=this.#e,s={...e};"number"==typeof n&&(s.next={...e?.next,revalidate:n});let o=new d("network");for(let e=0;e<=i;e++){let i;e>0&&await c(o.retryAfterMs??v(e-1,300,3e3));try{i=await y(r,t,s)}catch(e){o=new d(e instanceof Error&&"AbortError"===e.name?"timeout":"network",{cause:e});continue}if(!i){o=new d("timeout");continue}if(i.ok){try{this.config=w(await i.json())}catch(e){o=e instanceof d?e:new d("invalid-response",{cause:e});continue}return this.config}if(!(o=new d(g(i.status),{status:i.status,retryAfterMs:p(i.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=e=>{this.config=e},this.generateVisitorId=()=>[l,(function(e=5){return e&&"number"==typeof e?Array(e).fill("").reduce(e=>e+=m.charAt(Math.floor(Math.random()*m.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=e=>{this.#t||"u">typeof console&&console.warn(`[Improve] ${e}`)},this.validateTestValue=(e,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let r=this.config.tests[e];if(!r)throw Error(`No config found for ${e}`);return!!r.options.find(e=>e.slug===t)},this.validateVisitorId=e=>{let t=e.split("_");if(2!==t.length)return!1;let[r,i]=t;return r===l&&26===i.length},this.organizationId=e,this.environment=t,this.state=r,this._baseUrl=s||"https://improve.obelism.studio",this.#t=u??!1,i?this.config=i:this.#e={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:n||3e3,retries:o??2,revalidate:a}}}export{h as A,C as B,u as a,a as g,o as p};
2
- //# sourceMappingURL=base-i6scrpy2.mjs.map
1
+ import{UAParser as e}from"ua-parser-js";let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],r=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],i=["mac os","ios","android","windows","unix"],n=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],s=["wearable","mobile","tablet"],o=o=>{var a;if(!o||"string"!=typeof o)return null;let u=new e(o).getResult(),l=(a=u.device.type)&&t.includes(a)?a:"desktop";return{pointer:s.includes(l)?"coarse":"fine",device:l,browser:((e="")=>{if(!e)return"other";let t=e.toLowerCase(),i=r.find(e=>t.includes(e));return i||(n.includes(t)?"social":"other")})(u.browser.name),os:((e="")=>{if(!e)return"unix";let t=e.toLowerCase(),r=i.find(e=>t.includes(e));return r||"unix"})(u.os.name)}},a=(e,t)=>!e||Object.entries(e).every(([e,r])=>t[e]===r),u=e=>{let[t]=e;if(!t)return null;if(1===e.length)return t.slug;let r=Math.random()*e.reduce((e,{split:t})=>e+t,0);return(e.find(({split:e})=>(r-=e)<=0)||t).slug},l="visi",h="/api/log",c=e=>new Promise(t=>setTimeout(t,e)),f={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class d extends Error{constructor(e,t){super(f[e]),this.name="ImproveFetchError",this.reason=e,this.status=t?.status,this.retryAfterMs=t?.retryAfterMs,t&&"cause"in t&&(this.cause=t.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let g=e=>429===e?"rate-limited":401===e||403===e?"unauthorized":e>=500?"server":"client",m="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",w=e=>{if("object"!=typeof e||null===e||Array.isArray(e))throw new d("invalid-response");return{name:"string"==typeof e.name?e.name:"",version:"number"==typeof e.version?e.version:0,tests:e.tests??{},flags:e.flags??{},audience:e.audience??{}}},p=(e,t=Date.now())=>{if(!e)return;let r=Number(e);if(Number.isFinite(r))return Math.max(0,Math.round(1e3*r));let i=Date.parse(e);if(!Number.isNaN(i))return Math.max(0,i-t)},v=(e,t,r)=>Math.round(Math.random()*Math.min(r,t*2**e)),b=(e=3e3,t,r)=>{let i=new AbortController;return Promise.race([fetch(t,{...r,signal:i.signal}),(async(e=1e3,t)=>(await c(e),t?.abort(),null))(e,i)])};class y{#e;#t;constructor({organizationId:e,spaceId:t,environment:r,state:i,config:n,fetchTimeout:s,baseUrl:o,configRetries:a,configRevalidate:u,disableWarnings:h}){this.environment="develop",this.#e=null,this.#t=!1,this.config=null,this._fetchConfig=async e=>{if(this.config)return;if(!this.#e)throw Error("No config fetch setup provided");let{url:t,timeout:r,retries:i,revalidate:n}=this.#e,s={...e};"number"==typeof n&&(s.next={...e?.next,revalidate:n});let o=new d("network");for(let e=0;e<=i;e++){let i;e>0&&await c(o.retryAfterMs??v(e-1,300,3e3));try{i=await b(r,t,s)}catch(e){o=new d(e instanceof Error&&"AbortError"===e.name?"timeout":"network",{cause:e});continue}if(!i){o=new d("timeout");continue}if(i.ok){try{this.config=w(await i.json())}catch(e){o=e instanceof d?e:new d("invalid-response",{cause:e});continue}return this.config}if(!(o=new d(g(i.status),{status:i.status,retryAfterMs:p(i.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=e=>{this.config=e},this.generateVisitorId=()=>[l,(function(e=5){return e&&"number"==typeof e?Array(e).fill("").reduce(e=>e+=m.charAt(Math.floor(Math.random()*m.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=e=>{this.#t||"u">typeof console&&console.warn(`[Improve] ${e}`)},this.validateTestValue=(e,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let r=this.config.tests[e];if(!r)throw Error(`No config found for ${e}`);return!!r.options.find(e=>e.slug===t)},this.validateVisitorId=e=>{let t=e.split("_");if(2!==t.length)return!1;let[r,i]=t;return r===l&&26===i.length},this.organizationId=e,this.spaceId=t,this.environment=r,this.state=i,this._baseUrl=o||"https://improve.obelism.studio",this.#t=h??!1,n?this.config=n:this.#e={url:[`${this._baseUrl}/config`,this.spaceId||this.organizationId,this.environment,this.state||"active"].join("/"),timeout:s||3e3,retries:a??2,revalidate:u}}}export{h as A,y as B,u as a,a as g,o as p};
2
+ //# sourceMappingURL=base-e6yz3dpl.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-e6yz3dpl.mjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/config/urls.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/timeoutFetch.ts","../src/utils/getTimeoutError.ts","../src/base.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Location — coarse IP-geolocation (ISO 3166-1 alpha-2 country code). Values\n\t//? aren't enumerated here (any country code is valid); the key exists so\n\t//? audiences can target `country`. Resolved server-side (SSR/middleware) from\n\t//? the request, since the browser can't determine it from the user agent.\n\tcountry: [] as ReadonlyArray<string>,\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n\t// Coarse geo, not derived from the user agent — merged in server-side\n\t// (SSR/middleware) so audiences can target by country. Absent client-side.\n\tcountry?: string\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tconst [first] = options\n\tif (!first) return null\n\tif (options.length === 1) return first.slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || first\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\n// Events go to Obelism numbers (the numbers SDK's default endpoint); only a\n// custom `baseUrl` sends them to its own /api/log.\nexport const ANALYTICS_PATH = `/api/log`\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number | undefined\n\tretryAfterMs?: number | undefined\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: {\n\t\t\tstatus?: number | undefined\n\t\t\tretryAfterMs?: number | undefined\n\t\t\tcause?: unknown\n\t\t},\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number | undefined\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tspaceId: string | undefined\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState | undefined\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tspaceId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.spaceId = spaceId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\t// The space's datafile; the organization's id means its first\n\t\t\t\t\t// space.\n\t\t\t\t\tthis.spaceId || this.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","parseUserAgent","userAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","name","toLowerCase","browserTypeMatch","find","browserType","os","formatOs","osName","osTypeMatch","osType","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","first","length","slug","value","Math","random","reduce","acc","split","match","VISITOR_ID_PREFIX","ANALYTICS_PATH","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","min","timeoutFetch","url","controller","AbortController","race","fetch","signal","getTimeoutError","abort","BaseImproveSDK","organizationId","spaceId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl"],"mappings":"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"}
package/dist/client.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,"__esModule",{value:!0});var t=require("@obelism/numbers-sdk"),i=require("./base-gi7y83ve.js");let e=t=>{if(!t)return!1;let i=document.cookie.split("; ").find(i=>{let[e]=i.split("=");return t===e});return!!i&&i.split("=")[1]},s=(t,i)=>{let e=new Date;e.setDate(e.getDate()+30),document.cookie=`${t}=${i};path=/;expires=${e.toUTCString()};SameSite=Lax;Secure`};class r extends i.BaseImproveSDK{#t;#i;#e;#s;#r;#n;constructor(r){super(r),this.#i="",this.#e={},this.#r=!1,this.#n=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(t=window.navigator.userAgent)=>{let e=this.#s.visitor();if(!e)return null;this.#i=e.id;let s=i.parseUserAgent(t);if(!s)return null;this.#t=s;let{pointer:r,device:n,browser:o,os:a}=s;return this.#s.setDevice({pointer:r,device:n,browser:o,os:a}),this.#i},this.getFlagValue=t=>{if(!this.config)return null;let r=this.config.flags[t];if(!r||!r.options[0])return null;if(this.#t||this.setupVisitor(),!this.#i||!this.#t)return r.options[0].slug;if(this.#t?.[t])return this.#t[t];let n=e(t);if(n&&r.options.some(t=>t.slug===n))return this.#t[t]=n,this.#o("flag",r.id,n),n;if(!i.getVisitorMatchesAudience(this.config.audience[r.audience],this.#t))return r.options[0].slug;let o=i.getRandomTestValue(r.options);return o?(this.#t[t]=o,s(t,o),this.#o("flag",r.id,o),o):null},this.getTestValue=t=>{if(!this.config)return null;let r=this.config.tests[t];if(!r)return null;if(this.#t||this.setupVisitor(),!this.#i||!this.#t)return r.defaultValue;if(this.#t?.[t])return this.#t[t];let n=e(t);if(n&&this.validateTestValue(t,n))return this.#t[t]=n,this.#o("test",r.id,n),n;if(!i.getVisitorMatchesAudience(this.config.audience[r.audience],this.#t))return r.defaultValue;if(r.allocation<100&&100*Math.random()>r.allocation)return this.#t[t]=r.defaultValue,this.#t?.[t];let o=i.getRandomTestValue(r.options);return o?(this.#t[t]=o,s(t,o),this.#o("test",r.id,o),o):null},this.setAnalyticsUrls=t=>{this.#s.setEndpoint(t)},this.#o=(t,i,e)=>{if(this.#t&&this.#i)return this.#s.expose({subjectKind:t,subjectId:i,variant:e})},this.postAnalytic=(t,i,e)=>{if(!this.config||t.startsWith("gtm."))return null;i||this.#r||(this.#r=!0,this._warn(`postAnalytic("${t}", ...) was called with an empty scope. scope is required — pass a short, stable identifier for where/what this event refers to, e.g. "homepage_hero_login".`));let{value:s,currency:r,params:n,dedupeKey:o}=e??{},a="number"==typeof s&&Number.isFinite(s),u=n&&"object"==typeof n&&!Array.isArray(n)?n:void 0;this.#t||this.setupVisitor();let l=o?`${t}:${o}`:t;if(!this.#t||this.#e[l])return null;let h=this.#s.track(t,i,{...a?{value:s}:{},...r?{currency:r}:{},...u?{params:u}:{}});if(!h)return null;if(this.#e[l]=!0,this.#n){var d;u&&"ecommerce"in u&&"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push({ecommerce:null})),d={event:t,scope:i,improve:{visitorId:this.#i},...a?{value:s}:{},...r?{currency:r}:{},...u,_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(d))}return h},this.#s=t.createNumbers({organizationId:this.organizationId,environment:this.environment,endpoint:r.baseUrl?`${r.baseUrl}${i.ANALYTICS_PATH}`:t.DEFAULT_ENDPOINT,disableWarnings:r.disableWarnings??!1}),this.#n=r.dataLayer??!0}#o}exports.ImproveClientSDK=r;
1
+ Object.defineProperty(exports,"__esModule",{value:!0});var t=require("@obelism/numbers-sdk"),i=require("./base-8q51gsqe.js");let e=t=>{if(!t)return!1;let i=document.cookie.split("; ").find(i=>{let[e]=i.split("=");return t===e});return!!i&&i.split("=")[1]},s=(t,i)=>{let e=new Date;e.setDate(e.getDate()+30),document.cookie=`${t}=${i};path=/;expires=${e.toUTCString()};SameSite=Lax;Secure`};class r extends i.BaseImproveSDK{#t;#i;#e;#s;#r;#n;constructor(r){super(r),this.#i="",this.#e={},this.#r=!1,this.#n=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(t=window.navigator.userAgent)=>{let e=this.#s.visitor();if(!e)return null;this.#i=e.id;let s=i.parseUserAgent(t);if(!s)return null;this.#t=s;let{pointer:r,device:n,browser:o,os:a}=s;return this.#s.setDevice({pointer:r,device:n,browser:o,os:a}),this.#i},this.getFlagValue=t=>{if(!this.config)return null;let r=this.config.flags[t];if(!r||!r.options[0])return null;if(this.#t||this.setupVisitor(),!this.#i||!this.#t)return r.options[0].slug;if(this.#t?.[t])return this.#t[t];let n=e(t);if(n&&r.options.some(t=>t.slug===n))return this.#t[t]=n,this.#o("flag",r.id,n),n;if(!i.getVisitorMatchesAudience(this.config.audience[r.audience],this.#t))return r.options[0].slug;let o=i.getRandomTestValue(r.options);return o?(this.#t[t]=o,s(t,o),this.#o("flag",r.id,o),o):null},this.getTestValue=t=>{if(!this.config)return null;let r=this.config.tests[t];if(!r)return null;if(this.#t||this.setupVisitor(),!this.#i||!this.#t)return r.defaultValue;if(this.#t?.[t])return this.#t[t];let n=e(t);if(n&&this.validateTestValue(t,n))return this.#t[t]=n,this.#o("test",r.id,n),n;if(!i.getVisitorMatchesAudience(this.config.audience[r.audience],this.#t))return r.defaultValue;if(r.allocation<100&&100*Math.random()>r.allocation)return this.#t[t]=r.defaultValue,this.#t?.[t];let o=i.getRandomTestValue(r.options);return o?(this.#t[t]=o,s(t,o),this.#o("test",r.id,o),o):null},this.setAnalyticsUrls=t=>{this.#s.setEndpoint(t)},this.#o=(t,i,e)=>{if(this.#t&&this.#i)return this.#s.expose({subjectKind:t,subjectId:i,variant:e})},this.postAnalytic=(t,i,e)=>{if(!this.config||t.startsWith("gtm."))return null;i||this.#r||(this.#r=!0,this._warn(`postAnalytic("${t}", ...) was called with an empty scope. scope is required — pass a short, stable identifier for where/what this event refers to, e.g. "homepage_hero_login".`));let{value:s,currency:r,params:n,dedupeKey:o}=e??{},a="number"==typeof s&&Number.isFinite(s),u=n&&"object"==typeof n&&!Array.isArray(n)?n:void 0;this.#t||this.setupVisitor();let l=o?`${t}:${o}`:t;if(!this.#t||this.#e[l])return null;let h=this.#s.track(t,i,{...a?{value:s}:{},...r?{currency:r}:{},...u?{params:u}:{}});if(!h)return null;if(this.#e[l]=!0,this.#n){var d;u&&"ecommerce"in u&&"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push({ecommerce:null})),d={event:t,scope:i,improve:{visitorId:this.#i},...a?{value:s}:{},...r?{currency:r}:{},...u,_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(d))}return h},this.#s=t.createNumbers({organizationId:this.organizationId,spaceId:this.spaceId,environment:this.environment,endpoint:r.baseUrl?`${r.baseUrl}${i.ANALYTICS_PATH}`:t.DEFAULT_ENDPOINT,disableWarnings:r.disableWarnings??!1}),this.#n=r.dataLayer??!0}#o}exports.ImproveClientSDK=r;
2
2
  //# sourceMappingURL=client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","sources":["../src/utils/clientCookie.ts","../src/client.ts","../src/utils/pushDataLayer.ts"],"sourcesContent":["const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","import {\n\tcreateNumbers,\n\tDEFAULT_ENDPOINT,\n\ttype EventBeacon,\n\ttype ExposureBeacon,\n\ttype NumbersClient,\n} from '@obelism/numbers-sdk'\n\nimport { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport { ANALYTICS_PATH } from './config/urls'\nimport { pushDataLayer, resetDataLayerEcommerce } from './utils/pushDataLayer'\nimport {\n\tImproveAnalyticPayload,\n\tImproveEventName,\n\tImproveSetupArgs,\n} from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\n/**\n * A real-world tracked event. Test-independent — the server attributes it to a\n * test/flag at read time by joining the visitor's exposures. Sent through the\n * numbers SDK (`@obelism/numbers-sdk`), which adds the page's path and UTM tags.\n */\nexport type CreateEvent = EventBeacon\n\n/**\n * A visitor's assignment to a test/flag variant, emitted the first time the SDK\n * resolves a non-holdout value for them. The per-variant exposure count is the\n * denominator for results.\n */\nexport type CreateExposure = ExposureBeacon\n\n// Events already sent this instance, keyed by event name (or `event:dedupeKey`\n// when the caller passes one), so each fires at most once per page/session.\ntype TrackedEvents = {\n\t[event: string]: boolean\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorId: string = ''\n\n\t#analytics: TrackedEvents = {}\n\n\t// Events and exposures go to Obelism numbers through its SDK, which keeps\n\t// the visitor cookie, the device categories, the page context, the size\n\t// and field limits, and backs off after a 429.\n\t#numbers: NumbersClient\n\n\t// Fires the missing-scope warning at most once per SDK instance, not once\n\t// per call — a caller that forgets `scope` is likely forgetting it everywhere.\n\t#warnedMissingScope = false\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#numbers = createNumbers({\n\t\t\torganizationId: this.organizationId,\n\t\t\tenvironment: this.environment,\n\t\t\t// numbers takes the events; a custom `baseUrl` (e.g. a local Improve)\n\t\t\t// keeps getting them at its own /api/log.\n\t\t\tendpoint: args.baseUrl\n\t\t\t\t? `${args.baseUrl}${ANALYTICS_PATH}`\n\t\t\t\t: DEFAULT_ENDPOINT,\n\t\t\tdisableWarnings: args.disableWarnings ?? false,\n\t\t})\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\t// The numbers SDK reads (or makes) the visitor in the shared `visitorId`\n\t\t// cookie, so the visitor is the same one in numbers' own script.\n\t\tconst visitor = this.#numbers.visitor()\n\n\t\tif (!visitor) return null\n\n\t\tthis.#visitorId = visitor.id\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\t// The audiences' categories, so numbers reports the same ones.\n\t\tconst { pointer, device, browser, os } = parsedUserAgent\n\t\tthis.#numbers.setDevice({ pointer, device, browser, os })\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience,\n\t\t// keeping the visitor sticky and honoring a server-side (middleware)\n\t\t// country-targeted assignment.\n\t\tconst cookieFlagValue = getCookie(flagSlug)\n\t\tif (\n\t\t\tcookieFlagValue &&\n\t\t\tflagConfig.options.some((option) => option.slug === cookieFlagValue)\n\t\t) {\n\t\t\tthis.#visitor[flagSlug] = cookieFlagValue\n\t\t\tthis.#postExposure('flag', flagConfig.id, cookieFlagValue)\n\t\t\treturn cookieFlagValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this flag/variant server-side.\n\t\tthis.#postExposure('flag', flagConfig.id, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience\n\t\t// or allocation, so a bucketed visitor stays sticky. This is also how a\n\t\t// country-targeted test resolves in the browser: middleware bucketed the\n\t\t// visitor server-side (where geo is known) and set the cookie, and here we\n\t\t// honor it and record the exposure.\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tif (cookieTestValue && this.validateTestValue(testSlug, cookieTestValue)) {\n\t\t\tthis.#visitor[testSlug] = cookieTestValue\n\t\t\tthis.#postExposure('test', testConfig.id, cookieTestValue)\n\t\t\treturn cookieTestValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this test/variant server-side. Holdout (allocation) and audience-excluded\n\t\t// visitors return above without an exposure — they aren't in the test.\n\t\tthis.#postExposure('test', testConfig.id, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#numbers.setEndpoint(url)\n\t}\n\n\t// Emits an exposure beacon the first time a visitor is assigned to a subject\n\t// this instance (the numbers SDK sends each subject once). Idempotent\n\t// server-side (unique per visitor+subject).\n\t#postExposure = (\n\t\tsubjectKind: 'test' | 'flag',\n\t\tsubjectId: string,\n\t\tvariant: string,\n\t) => {\n\t\tif (!this.#visitor || !this.#visitorId) return\n\n\t\treturn this.#numbers.expose({ subjectKind, subjectId, variant })\n\t}\n\n\tpostAnalytic = (\n\t\tevent: ImproveEventName,\n\t\t// Required, freeform, but always present — the stable second half of the\n\t\t// event's identity (e.g. \"homepage_hero_login\"). Not a display string;\n\t\t// keep it structured and consistent, unlike the old `message` field.\n\t\tscope: string,\n\t\tpayload?: ImproveAnalyticPayload,\n\t) => {\n\t\tif (!this.config) return null\n\n\t\t// GTM reserves the `gtm.*` namespace for its own events; never emit into\n\t\t// it, or the mirrored dataLayer entry would collide with GTM internals.\n\t\tif (event.startsWith('gtm.')) return null\n\n\t\t// The numbers SDK nudges toward snake_case names itself; an empty scope\n\t\t// is improve's own convention to warn about (numbers falls back to the\n\t\t// page's path).\n\t\tif (!scope && !this.#warnedMissingScope) {\n\t\t\tthis.#warnedMissingScope = true\n\t\t\tthis._warn(\n\t\t\t\t`postAnalytic(\"${event}\", ...) was called with an empty scope. ` +\n\t\t\t\t\t`scope is required — pass a short, stable identifier for where/what ` +\n\t\t\t\t\t`this event refers to, e.g. \"homepage_hero_login\".`,\n\t\t\t)\n\t\t}\n\n\t\tconst { value, currency, params, dedupeKey }: ImproveAnalyticPayload =\n\t\t\tpayload ?? {}\n\t\tconst hasValue = typeof value === 'number' && Number.isFinite(value)\n\t\tconst validParams =\n\t\t\tparams && typeof params === 'object' && !Array.isArray(params)\n\t\t\t\t? params\n\t\t\t\t: undefined\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\t// The event is test-independent: it's recorded once per visitor and later\n\t\t// attributed to whichever tests/flags the visitor was exposed to. Dedup per\n\t\t// event name (or per `event:dedupeKey`, when the caller distinguishes\n\t\t// repeated firings of the same name) so it fires at most once per\n\t\t// page/session.\n\t\tconst trackingKey = dedupeKey ? `${event}:${dedupeKey}` : event\n\t\tif (!this.#visitor || this.#analytics[trackingKey]) return null\n\n\t\tconst request = this.#numbers.track(event, scope, {\n\t\t\t...(hasValue ? { value } : {}),\n\t\t\t...(currency ? { currency } : {}),\n\t\t\t...(validParams ? { params: validParams } : {}),\n\t\t})\n\n\t\t// Nothing sent (paused after a 429): don't mark the event as tracked, so\n\t\t// it can still fire once the cooldown elapses.\n\t\tif (!request) return null\n\n\t\tthis.#analytics[trackingKey] = true\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\t// Clear any prior ecommerce object first so its keys don't bleed into\n\t\t\t// this event (GTM/GA4 best practice).\n\t\t\tif (validParams && 'ecommerce' in validParams) resetDataLayerEcommerce()\n\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\tscope,\n\t\t\t\timprove: {\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t...(hasValue ? { value } : {}),\n\t\t\t\t...(currency ? { currency } : {}),\n\t\t\t\t// Spread custom params (incl. a GA4 `ecommerce` object) to the top\n\t\t\t\t// level so GA4/GTM tags can read them directly.\n\t\t\t\t...validParams,\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\treturn request\n\t}\n}\n","import { ImproveEventName } from '../types'\n\nexport type ImproveDataLayerEntry = {\n\tevent: ImproveEventName\n\timprove: {\n\t\t// Events are test-independent; attribution to a test/variant happens\n\t\t// server-side by joining the visitor's exposures, so only the visitor is\n\t\t// carried here.\n\t\tvisitorId: string\n\t}\n\t/** GA4 numeric value / currency, when provided on the analytic. */\n\tvalue?: number\n\tcurrency?: string\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n\t/** Any extra event params (e.g. a GA4 `ecommerce` object) spread onto the entry. */\n\t[param: string]: unknown\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n\n/**\n * Clear the dataLayer `ecommerce` object before pushing an ecommerce event, so\n * leftover keys (old `items`, a stale `coupon`…) from a previous event don't\n * bleed into the next one — the GTM/GA4 recommended reset. No-op outside the\n * browser.\n */\nexport const resetDataLayerEcommerce = () => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push({ ecommerce: null })\n}\n"],"names":["getCookie","name","cookie","document","split","find","row","key","setCookie","value","now","Date","setDate","getDate","toUTCString","ImproveClientSDK","BaseImproveSDK","args","fetchConfig","_fetchConfig","setupVisitor","userAgent","window","navigator","visitor","id","parsedUserAgent","parseUserAgent","pointer","device","browser","os","setDevice","getFlagValue","flagSlug","config","flagConfig","flags","options","slug","cookieFlagValue","some","option","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testSlug","testConfig","tests","defaultValue","cookieTestValue","validateTestValue","allocation","Math","random","testValue","setAnalyticsUrls","url","setEndpoint","subjectKind","subjectId","variant","expose","postAnalytic","event","scope","payload","startsWith","_warn","currency","params","dedupeKey","hasValue","Number","isFinite","validParams","Array","isArray","undefined","trackingKey","request","track","entry","dataLayer","push","ecommerce","improve","visitorId","_improve","createNumbers","organizationId","environment","endpoint","baseUrl","ANALYTICS_PATH","DEFAULT_ENDPOINT","disableWarnings"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","sources":["../src/utils/clientCookie.ts","../src/client.ts","../src/utils/pushDataLayer.ts"],"sourcesContent":["const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","import {\n\tcreateNumbers,\n\tDEFAULT_ENDPOINT,\n\ttype EventBeacon,\n\ttype ExposureBeacon,\n\ttype NumbersClient,\n} from '@obelism/numbers-sdk'\n\nimport { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport { ANALYTICS_PATH } from './config/urls'\nimport { pushDataLayer, resetDataLayerEcommerce } from './utils/pushDataLayer'\nimport {\n\tImproveAnalyticPayload,\n\tImproveEventName,\n\tImproveSetupArgs,\n} from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\n/**\n * A real-world tracked event. Test-independent — the server attributes it to a\n * test/flag at read time by joining the visitor's exposures. Sent through the\n * numbers SDK (`@obelism/numbers-sdk`), which adds the page's path and UTM tags.\n */\nexport type CreateEvent = EventBeacon\n\n/**\n * A visitor's assignment to a test/flag variant, emitted the first time the SDK\n * resolves a non-holdout value for them. The per-variant exposure count is the\n * denominator for results.\n */\nexport type CreateExposure = ExposureBeacon\n\n// Events already sent this instance, keyed by event name (or `event:dedupeKey`\n// when the caller passes one), so each fires at most once per page/session.\ntype TrackedEvents = {\n\t[event: string]: boolean\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorId: string = ''\n\n\t#analytics: TrackedEvents = {}\n\n\t// Events and exposures go to Obelism numbers through its SDK, which keeps\n\t// the visitor cookie, the device categories, the page context, the size\n\t// and field limits, and backs off after a 429.\n\t#numbers: NumbersClient\n\n\t// Fires the missing-scope warning at most once per SDK instance, not once\n\t// per call — a caller that forgets `scope` is likely forgetting it everywhere.\n\t#warnedMissingScope = false\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#numbers = createNumbers({\n\t\t\torganizationId: this.organizationId,\n\t\t\tspaceId: this.spaceId,\n\t\t\tenvironment: this.environment,\n\t\t\t// numbers takes the events; a custom `baseUrl` (e.g. a local Improve)\n\t\t\t// keeps getting them at its own /api/log.\n\t\t\tendpoint: args.baseUrl\n\t\t\t\t? `${args.baseUrl}${ANALYTICS_PATH}`\n\t\t\t\t: DEFAULT_ENDPOINT,\n\t\t\tdisableWarnings: args.disableWarnings ?? false,\n\t\t})\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\t// The numbers SDK reads (or makes) the visitor in the shared `visitorId`\n\t\t// cookie, so the visitor is the same one in numbers' own script.\n\t\tconst visitor = this.#numbers.visitor()\n\n\t\tif (!visitor) return null\n\n\t\tthis.#visitorId = visitor.id\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\t// The audiences' categories, so numbers reports the same ones.\n\t\tconst { pointer, device, browser, os } = parsedUserAgent\n\t\tthis.#numbers.setDevice({ pointer, device, browser, os })\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience,\n\t\t// keeping the visitor sticky and honoring a server-side (middleware)\n\t\t// country-targeted assignment.\n\t\tconst cookieFlagValue = getCookie(flagSlug)\n\t\tif (\n\t\t\tcookieFlagValue &&\n\t\t\tflagConfig.options.some((option) => option.slug === cookieFlagValue)\n\t\t) {\n\t\t\tthis.#visitor[flagSlug] = cookieFlagValue\n\t\t\tthis.#postExposure('flag', flagConfig.id, cookieFlagValue)\n\t\t\treturn cookieFlagValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this flag/variant server-side.\n\t\tthis.#postExposure('flag', flagConfig.id, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience\n\t\t// or allocation, so a bucketed visitor stays sticky. This is also how a\n\t\t// country-targeted test resolves in the browser: middleware bucketed the\n\t\t// visitor server-side (where geo is known) and set the cookie, and here we\n\t\t// honor it and record the exposure.\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tif (cookieTestValue && this.validateTestValue(testSlug, cookieTestValue)) {\n\t\t\tthis.#visitor[testSlug] = cookieTestValue\n\t\t\tthis.#postExposure('test', testConfig.id, cookieTestValue)\n\t\t\treturn cookieTestValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this test/variant server-side. Holdout (allocation) and audience-excluded\n\t\t// visitors return above without an exposure — they aren't in the test.\n\t\tthis.#postExposure('test', testConfig.id, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#numbers.setEndpoint(url)\n\t}\n\n\t// Emits an exposure beacon the first time a visitor is assigned to a subject\n\t// this instance (the numbers SDK sends each subject once). Idempotent\n\t// server-side (unique per visitor+subject).\n\t#postExposure = (\n\t\tsubjectKind: 'test' | 'flag',\n\t\tsubjectId: string,\n\t\tvariant: string,\n\t) => {\n\t\tif (!this.#visitor || !this.#visitorId) return\n\n\t\treturn this.#numbers.expose({ subjectKind, subjectId, variant })\n\t}\n\n\tpostAnalytic = (\n\t\tevent: ImproveEventName,\n\t\t// Required, freeform, but always present — the stable second half of the\n\t\t// event's identity (e.g. \"homepage_hero_login\"). Not a display string;\n\t\t// keep it structured and consistent, unlike the old `message` field.\n\t\tscope: string,\n\t\tpayload?: ImproveAnalyticPayload,\n\t) => {\n\t\tif (!this.config) return null\n\n\t\t// GTM reserves the `gtm.*` namespace for its own events; never emit into\n\t\t// it, or the mirrored dataLayer entry would collide with GTM internals.\n\t\tif (event.startsWith('gtm.')) return null\n\n\t\t// The numbers SDK nudges toward snake_case names itself; an empty scope\n\t\t// is improve's own convention to warn about (numbers falls back to the\n\t\t// page's path).\n\t\tif (!scope && !this.#warnedMissingScope) {\n\t\t\tthis.#warnedMissingScope = true\n\t\t\tthis._warn(\n\t\t\t\t`postAnalytic(\"${event}\", ...) was called with an empty scope. ` +\n\t\t\t\t\t`scope is required — pass a short, stable identifier for where/what ` +\n\t\t\t\t\t`this event refers to, e.g. \"homepage_hero_login\".`,\n\t\t\t)\n\t\t}\n\n\t\tconst { value, currency, params, dedupeKey }: ImproveAnalyticPayload =\n\t\t\tpayload ?? {}\n\t\tconst hasValue = typeof value === 'number' && Number.isFinite(value)\n\t\tconst validParams =\n\t\t\tparams && typeof params === 'object' && !Array.isArray(params)\n\t\t\t\t? params\n\t\t\t\t: undefined\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\t// The event is test-independent: it's recorded once per visitor and later\n\t\t// attributed to whichever tests/flags the visitor was exposed to. Dedup per\n\t\t// event name (or per `event:dedupeKey`, when the caller distinguishes\n\t\t// repeated firings of the same name) so it fires at most once per\n\t\t// page/session.\n\t\tconst trackingKey = dedupeKey ? `${event}:${dedupeKey}` : event\n\t\tif (!this.#visitor || this.#analytics[trackingKey]) return null\n\n\t\tconst request = this.#numbers.track(event, scope, {\n\t\t\t...(hasValue ? { value } : {}),\n\t\t\t...(currency ? { currency } : {}),\n\t\t\t...(validParams ? { params: validParams } : {}),\n\t\t})\n\n\t\t// Nothing sent (paused after a 429): don't mark the event as tracked, so\n\t\t// it can still fire once the cooldown elapses.\n\t\tif (!request) return null\n\n\t\tthis.#analytics[trackingKey] = true\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\t// Clear any prior ecommerce object first so its keys don't bleed into\n\t\t\t// this event (GTM/GA4 best practice).\n\t\t\tif (validParams && 'ecommerce' in validParams) resetDataLayerEcommerce()\n\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\tscope,\n\t\t\t\timprove: {\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t...(hasValue ? { value } : {}),\n\t\t\t\t...(currency ? { currency } : {}),\n\t\t\t\t// Spread custom params (incl. a GA4 `ecommerce` object) to the top\n\t\t\t\t// level so GA4/GTM tags can read them directly.\n\t\t\t\t...validParams,\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\treturn request\n\t}\n}\n","import { ImproveEventName } from '../types'\n\nexport type ImproveDataLayerEntry = {\n\tevent: ImproveEventName\n\timprove: {\n\t\t// Events are test-independent; attribution to a test/variant happens\n\t\t// server-side by joining the visitor's exposures, so only the visitor is\n\t\t// carried here.\n\t\tvisitorId: string\n\t}\n\t/** GA4 numeric value / currency, when provided on the analytic. */\n\tvalue?: number\n\tcurrency?: string\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n\t/** Any extra event params (e.g. a GA4 `ecommerce` object) spread onto the entry. */\n\t[param: string]: unknown\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n\n/**\n * Clear the dataLayer `ecommerce` object before pushing an ecommerce event, so\n * leftover keys (old `items`, a stale `coupon`…) from a previous event don't\n * bleed into the next one — the GTM/GA4 recommended reset. No-op outside the\n * browser.\n */\nexport const resetDataLayerEcommerce = () => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push({ ecommerce: null })\n}\n"],"names":["getCookie","name","cookie","document","split","find","row","key","setCookie","value","now","Date","setDate","getDate","toUTCString","ImproveClientSDK","BaseImproveSDK","args","fetchConfig","_fetchConfig","setupVisitor","userAgent","window","navigator","visitor","id","parsedUserAgent","parseUserAgent","pointer","device","browser","os","setDevice","getFlagValue","flagSlug","config","flagConfig","flags","options","slug","cookieFlagValue","some","option","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testSlug","testConfig","tests","defaultValue","cookieTestValue","validateTestValue","allocation","Math","random","testValue","setAnalyticsUrls","url","setEndpoint","subjectKind","subjectId","variant","expose","postAnalytic","event","scope","payload","startsWith","_warn","currency","params","dedupeKey","hasValue","Number","isFinite","validParams","Array","isArray","undefined","trackingKey","request","track","entry","dataLayer","push","ecommerce","improve","visitorId","_improve","createNumbers","organizationId","spaceId","environment","endpoint","baseUrl","ANALYTICS_PATH","DEFAULT_ENDPOINT","disableWarnings"],"mappings":"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"}
package/dist/client.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{createNumbers as i,DEFAULT_ENDPOINT as t}from"@obelism/numbers-sdk";import{B as s,p as e,g as r,a as o,A as n}from"./base-i6scrpy2.mjs";let a=i=>{if(!i)return!1;let t=document.cookie.split("; ").find(t=>{let[s]=t.split("=");return i===s});return!!t&&t.split("=")[1]},l=(i,t)=>{let s=new Date;s.setDate(s.getDate()+30),document.cookie=`${i}=${t};path=/;expires=${s.toUTCString()};SameSite=Lax;Secure`};class u extends s{#i;#t;#s;#e;#r;#o;constructor(s){super(s),this.#t="",this.#s={},this.#r=!1,this.#o=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(i=window.navigator.userAgent)=>{let t=this.#e.visitor();if(!t)return null;this.#t=t.id;let s=e(i);if(!s)return null;this.#i=s;let{pointer:r,device:o,browser:n,os:a}=s;return this.#e.setDevice({pointer:r,device:o,browser:n,os:a}),this.#t},this.getFlagValue=i=>{if(!this.config)return null;let t=this.config.flags[i];if(!t||!t.options[0])return null;if(this.#i||this.setupVisitor(),!this.#t||!this.#i)return t.options[0].slug;if(this.#i?.[i])return this.#i[i];let s=a(i);if(s&&t.options.some(i=>i.slug===s))return this.#i[i]=s,this.#n("flag",t.id,s),s;if(!r(this.config.audience[t.audience],this.#i))return t.options[0].slug;let e=o(t.options);return e?(this.#i[i]=e,l(i,e),this.#n("flag",t.id,e),e):null},this.getTestValue=i=>{if(!this.config)return null;let t=this.config.tests[i];if(!t)return null;if(this.#i||this.setupVisitor(),!this.#t||!this.#i)return t.defaultValue;if(this.#i?.[i])return this.#i[i];let s=a(i);if(s&&this.validateTestValue(i,s))return this.#i[i]=s,this.#n("test",t.id,s),s;if(!r(this.config.audience[t.audience],this.#i))return t.defaultValue;if(t.allocation<100&&100*Math.random()>t.allocation)return this.#i[i]=t.defaultValue,this.#i?.[i];let e=o(t.options);return e?(this.#i[i]=e,l(i,e),this.#n("test",t.id,e),e):null},this.setAnalyticsUrls=i=>{this.#e.setEndpoint(i)},this.#n=(i,t,s)=>{if(this.#i&&this.#t)return this.#e.expose({subjectKind:i,subjectId:t,variant:s})},this.postAnalytic=(i,t,s)=>{if(!this.config||i.startsWith("gtm."))return null;t||this.#r||(this.#r=!0,this._warn(`postAnalytic("${i}", ...) was called with an empty scope. scope is required — pass a short, stable identifier for where/what this event refers to, e.g. "homepage_hero_login".`));let{value:e,currency:r,params:o,dedupeKey:n}=s??{},a="number"==typeof e&&Number.isFinite(e),l=o&&"object"==typeof o&&!Array.isArray(o)?o:void 0;this.#i||this.setupVisitor();let u=n?`${i}:${n}`:i;if(!this.#i||this.#s[u])return null;let h=this.#e.track(i,t,{...a?{value:e}:{},...r?{currency:r}:{},...l?{params:l}:{}});if(!h)return null;if(this.#s[u]=!0,this.#o){var d;l&&"ecommerce"in l&&"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push({ecommerce:null})),d={event:i,scope:t,improve:{visitorId:this.#t},...a?{value:e}:{},...r?{currency:r}:{},...l,_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(d))}return h},this.#e=i({organizationId:this.organizationId,environment:this.environment,endpoint:s.baseUrl?`${s.baseUrl}${n}`:t,disableWarnings:s.disableWarnings??!1}),this.#o=s.dataLayer??!0}#n}export{u as ImproveClientSDK};
1
+ import{createNumbers as i,DEFAULT_ENDPOINT as t}from"@obelism/numbers-sdk";import{B as s,p as e,g as r,a as o,A as n}from"./base-e6yz3dpl.mjs";let a=i=>{if(!i)return!1;let t=document.cookie.split("; ").find(t=>{let[s]=t.split("=");return i===s});return!!t&&t.split("=")[1]},l=(i,t)=>{let s=new Date;s.setDate(s.getDate()+30),document.cookie=`${i}=${t};path=/;expires=${s.toUTCString()};SameSite=Lax;Secure`};class u extends s{#i;#t;#s;#e;#r;#o;constructor(s){super(s),this.#t="",this.#s={},this.#r=!1,this.#o=!0,this.fetchConfig=this._fetchConfig,this.setupVisitor=(i=window.navigator.userAgent)=>{let t=this.#e.visitor();if(!t)return null;this.#t=t.id;let s=e(i);if(!s)return null;this.#i=s;let{pointer:r,device:o,browser:n,os:a}=s;return this.#e.setDevice({pointer:r,device:o,browser:n,os:a}),this.#t},this.getFlagValue=i=>{if(!this.config)return null;let t=this.config.flags[i];if(!t||!t.options[0])return null;if(this.#i||this.setupVisitor(),!this.#t||!this.#i)return t.options[0].slug;if(this.#i?.[i])return this.#i[i];let s=a(i);if(s&&t.options.some(i=>i.slug===s))return this.#i[i]=s,this.#n("flag",t.id,s),s;if(!r(this.config.audience[t.audience],this.#i))return t.options[0].slug;let e=o(t.options);return e?(this.#i[i]=e,l(i,e),this.#n("flag",t.id,e),e):null},this.getTestValue=i=>{if(!this.config)return null;let t=this.config.tests[i];if(!t)return null;if(this.#i||this.setupVisitor(),!this.#t||!this.#i)return t.defaultValue;if(this.#i?.[i])return this.#i[i];let s=a(i);if(s&&this.validateTestValue(i,s))return this.#i[i]=s,this.#n("test",t.id,s),s;if(!r(this.config.audience[t.audience],this.#i))return t.defaultValue;if(t.allocation<100&&100*Math.random()>t.allocation)return this.#i[i]=t.defaultValue,this.#i?.[i];let e=o(t.options);return e?(this.#i[i]=e,l(i,e),this.#n("test",t.id,e),e):null},this.setAnalyticsUrls=i=>{this.#e.setEndpoint(i)},this.#n=(i,t,s)=>{if(this.#i&&this.#t)return this.#e.expose({subjectKind:i,subjectId:t,variant:s})},this.postAnalytic=(i,t,s)=>{if(!this.config||i.startsWith("gtm."))return null;t||this.#r||(this.#r=!0,this._warn(`postAnalytic("${i}", ...) was called with an empty scope. scope is required — pass a short, stable identifier for where/what this event refers to, e.g. "homepage_hero_login".`));let{value:e,currency:r,params:o,dedupeKey:n}=s??{},a="number"==typeof e&&Number.isFinite(e),l=o&&"object"==typeof o&&!Array.isArray(o)?o:void 0;this.#i||this.setupVisitor();let u=n?`${i}:${n}`:i;if(!this.#i||this.#s[u])return null;let h=this.#e.track(i,t,{...a?{value:e}:{},...r?{currency:r}:{},...l?{params:l}:{}});if(!h)return null;if(this.#s[u]=!0,this.#o){var d;l&&"ecommerce"in l&&"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push({ecommerce:null})),d={event:i,scope:t,improve:{visitorId:this.#t},...a?{value:e}:{},...r?{currency:r}:{},...l,_improve:!0},"u">typeof window&&(window.dataLayer=window.dataLayer||[],window.dataLayer.push(d))}return h},this.#e=i({organizationId:this.organizationId,spaceId:this.spaceId,environment:this.environment,endpoint:s.baseUrl?`${s.baseUrl}${n}`:t,disableWarnings:s.disableWarnings??!1}),this.#o=s.dataLayer??!0}#n}export{u as ImproveClientSDK};
2
2
  //# sourceMappingURL=client.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","sources":["../src/utils/clientCookie.ts","../src/client.ts","../src/utils/pushDataLayer.ts"],"sourcesContent":["const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","import {\n\tcreateNumbers,\n\tDEFAULT_ENDPOINT,\n\ttype EventBeacon,\n\ttype ExposureBeacon,\n\ttype NumbersClient,\n} from '@obelism/numbers-sdk'\n\nimport { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport { ANALYTICS_PATH } from './config/urls'\nimport { pushDataLayer, resetDataLayerEcommerce } from './utils/pushDataLayer'\nimport {\n\tImproveAnalyticPayload,\n\tImproveEventName,\n\tImproveSetupArgs,\n} from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\n/**\n * A real-world tracked event. Test-independent — the server attributes it to a\n * test/flag at read time by joining the visitor's exposures. Sent through the\n * numbers SDK (`@obelism/numbers-sdk`), which adds the page's path and UTM tags.\n */\nexport type CreateEvent = EventBeacon\n\n/**\n * A visitor's assignment to a test/flag variant, emitted the first time the SDK\n * resolves a non-holdout value for them. The per-variant exposure count is the\n * denominator for results.\n */\nexport type CreateExposure = ExposureBeacon\n\n// Events already sent this instance, keyed by event name (or `event:dedupeKey`\n// when the caller passes one), so each fires at most once per page/session.\ntype TrackedEvents = {\n\t[event: string]: boolean\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorId: string = ''\n\n\t#analytics: TrackedEvents = {}\n\n\t// Events and exposures go to Obelism numbers through its SDK, which keeps\n\t// the visitor cookie, the device categories, the page context, the size\n\t// and field limits, and backs off after a 429.\n\t#numbers: NumbersClient\n\n\t// Fires the missing-scope warning at most once per SDK instance, not once\n\t// per call — a caller that forgets `scope` is likely forgetting it everywhere.\n\t#warnedMissingScope = false\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#numbers = createNumbers({\n\t\t\torganizationId: this.organizationId,\n\t\t\tenvironment: this.environment,\n\t\t\t// numbers takes the events; a custom `baseUrl` (e.g. a local Improve)\n\t\t\t// keeps getting them at its own /api/log.\n\t\t\tendpoint: args.baseUrl\n\t\t\t\t? `${args.baseUrl}${ANALYTICS_PATH}`\n\t\t\t\t: DEFAULT_ENDPOINT,\n\t\t\tdisableWarnings: args.disableWarnings ?? false,\n\t\t})\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\t// The numbers SDK reads (or makes) the visitor in the shared `visitorId`\n\t\t// cookie, so the visitor is the same one in numbers' own script.\n\t\tconst visitor = this.#numbers.visitor()\n\n\t\tif (!visitor) return null\n\n\t\tthis.#visitorId = visitor.id\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\t// The audiences' categories, so numbers reports the same ones.\n\t\tconst { pointer, device, browser, os } = parsedUserAgent\n\t\tthis.#numbers.setDevice({ pointer, device, browser, os })\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience,\n\t\t// keeping the visitor sticky and honoring a server-side (middleware)\n\t\t// country-targeted assignment.\n\t\tconst cookieFlagValue = getCookie(flagSlug)\n\t\tif (\n\t\t\tcookieFlagValue &&\n\t\t\tflagConfig.options.some((option) => option.slug === cookieFlagValue)\n\t\t) {\n\t\t\tthis.#visitor[flagSlug] = cookieFlagValue\n\t\t\tthis.#postExposure('flag', flagConfig.id, cookieFlagValue)\n\t\t\treturn cookieFlagValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this flag/variant server-side.\n\t\tthis.#postExposure('flag', flagConfig.id, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience\n\t\t// or allocation, so a bucketed visitor stays sticky. This is also how a\n\t\t// country-targeted test resolves in the browser: middleware bucketed the\n\t\t// visitor server-side (where geo is known) and set the cookie, and here we\n\t\t// honor it and record the exposure.\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tif (cookieTestValue && this.validateTestValue(testSlug, cookieTestValue)) {\n\t\t\tthis.#visitor[testSlug] = cookieTestValue\n\t\t\tthis.#postExposure('test', testConfig.id, cookieTestValue)\n\t\t\treturn cookieTestValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this test/variant server-side. Holdout (allocation) and audience-excluded\n\t\t// visitors return above without an exposure — they aren't in the test.\n\t\tthis.#postExposure('test', testConfig.id, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#numbers.setEndpoint(url)\n\t}\n\n\t// Emits an exposure beacon the first time a visitor is assigned to a subject\n\t// this instance (the numbers SDK sends each subject once). Idempotent\n\t// server-side (unique per visitor+subject).\n\t#postExposure = (\n\t\tsubjectKind: 'test' | 'flag',\n\t\tsubjectId: string,\n\t\tvariant: string,\n\t) => {\n\t\tif (!this.#visitor || !this.#visitorId) return\n\n\t\treturn this.#numbers.expose({ subjectKind, subjectId, variant })\n\t}\n\n\tpostAnalytic = (\n\t\tevent: ImproveEventName,\n\t\t// Required, freeform, but always present — the stable second half of the\n\t\t// event's identity (e.g. \"homepage_hero_login\"). Not a display string;\n\t\t// keep it structured and consistent, unlike the old `message` field.\n\t\tscope: string,\n\t\tpayload?: ImproveAnalyticPayload,\n\t) => {\n\t\tif (!this.config) return null\n\n\t\t// GTM reserves the `gtm.*` namespace for its own events; never emit into\n\t\t// it, or the mirrored dataLayer entry would collide with GTM internals.\n\t\tif (event.startsWith('gtm.')) return null\n\n\t\t// The numbers SDK nudges toward snake_case names itself; an empty scope\n\t\t// is improve's own convention to warn about (numbers falls back to the\n\t\t// page's path).\n\t\tif (!scope && !this.#warnedMissingScope) {\n\t\t\tthis.#warnedMissingScope = true\n\t\t\tthis._warn(\n\t\t\t\t`postAnalytic(\"${event}\", ...) was called with an empty scope. ` +\n\t\t\t\t\t`scope is required — pass a short, stable identifier for where/what ` +\n\t\t\t\t\t`this event refers to, e.g. \"homepage_hero_login\".`,\n\t\t\t)\n\t\t}\n\n\t\tconst { value, currency, params, dedupeKey }: ImproveAnalyticPayload =\n\t\t\tpayload ?? {}\n\t\tconst hasValue = typeof value === 'number' && Number.isFinite(value)\n\t\tconst validParams =\n\t\t\tparams && typeof params === 'object' && !Array.isArray(params)\n\t\t\t\t? params\n\t\t\t\t: undefined\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\t// The event is test-independent: it's recorded once per visitor and later\n\t\t// attributed to whichever tests/flags the visitor was exposed to. Dedup per\n\t\t// event name (or per `event:dedupeKey`, when the caller distinguishes\n\t\t// repeated firings of the same name) so it fires at most once per\n\t\t// page/session.\n\t\tconst trackingKey = dedupeKey ? `${event}:${dedupeKey}` : event\n\t\tif (!this.#visitor || this.#analytics[trackingKey]) return null\n\n\t\tconst request = this.#numbers.track(event, scope, {\n\t\t\t...(hasValue ? { value } : {}),\n\t\t\t...(currency ? { currency } : {}),\n\t\t\t...(validParams ? { params: validParams } : {}),\n\t\t})\n\n\t\t// Nothing sent (paused after a 429): don't mark the event as tracked, so\n\t\t// it can still fire once the cooldown elapses.\n\t\tif (!request) return null\n\n\t\tthis.#analytics[trackingKey] = true\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\t// Clear any prior ecommerce object first so its keys don't bleed into\n\t\t\t// this event (GTM/GA4 best practice).\n\t\t\tif (validParams && 'ecommerce' in validParams) resetDataLayerEcommerce()\n\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\tscope,\n\t\t\t\timprove: {\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t...(hasValue ? { value } : {}),\n\t\t\t\t...(currency ? { currency } : {}),\n\t\t\t\t// Spread custom params (incl. a GA4 `ecommerce` object) to the top\n\t\t\t\t// level so GA4/GTM tags can read them directly.\n\t\t\t\t...validParams,\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\treturn request\n\t}\n}\n","import { ImproveEventName } from '../types'\n\nexport type ImproveDataLayerEntry = {\n\tevent: ImproveEventName\n\timprove: {\n\t\t// Events are test-independent; attribution to a test/variant happens\n\t\t// server-side by joining the visitor's exposures, so only the visitor is\n\t\t// carried here.\n\t\tvisitorId: string\n\t}\n\t/** GA4 numeric value / currency, when provided on the analytic. */\n\tvalue?: number\n\tcurrency?: string\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n\t/** Any extra event params (e.g. a GA4 `ecommerce` object) spread onto the entry. */\n\t[param: string]: unknown\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n\n/**\n * Clear the dataLayer `ecommerce` object before pushing an ecommerce event, so\n * leftover keys (old `items`, a stale `coupon`…) from a previous event don't\n * bleed into the next one — the GTM/GA4 recommended reset. No-op outside the\n * browser.\n */\nexport const resetDataLayerEcommerce = () => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push({ ecommerce: null })\n}\n"],"names":["getCookie","name","cookie","document","split","find","row","key","setCookie","value","now","Date","setDate","getDate","toUTCString","ImproveClientSDK","BaseImproveSDK","args","fetchConfig","_fetchConfig","setupVisitor","userAgent","window","navigator","visitor","id","parsedUserAgent","parseUserAgent","pointer","device","browser","os","setDevice","getFlagValue","flagSlug","config","flagConfig","flags","options","slug","cookieFlagValue","some","option","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testSlug","testConfig","tests","defaultValue","cookieTestValue","validateTestValue","allocation","Math","random","testValue","setAnalyticsUrls","url","setEndpoint","subjectKind","subjectId","variant","expose","postAnalytic","event","scope","payload","startsWith","_warn","currency","params","dedupeKey","hasValue","Number","isFinite","validParams","Array","isArray","undefined","trackingKey","request","track","entry","dataLayer","push","ecommerce","improve","visitorId","_improve","createNumbers","organizationId","environment","endpoint","baseUrl","ANALYTICS_PATH","DEFAULT_ENDPOINT","disableWarnings"],"mappings":"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"}
1
+ {"version":3,"file":"client.mjs","sources":["../src/utils/clientCookie.ts","../src/client.ts","../src/utils/pushDataLayer.ts"],"sourcesContent":["const ROW_DELIMITER = '; '\nconst ENTRY_DELIMITER = '='\n\nexport const getCookie = (name: string) => {\n\tif (!name) return false\n\tconst cookie = document.cookie.split(ROW_DELIMITER).find((row) => {\n\t\tconst [key] = row.split(ENTRY_DELIMITER)\n\t\treturn name === key\n\t})\n\treturn cookie ? cookie.split(ENTRY_DELIMITER)[1] : false\n}\n\nexport const setCookie = (name: string, value: string) => {\n\tconst now = new Date()\n\tnow.setDate(now.getDate() + 30)\n\tdocument.cookie = `${name}=${value};path=/;expires=${now.toUTCString()};SameSite=Lax;Secure`\n}\n","import {\n\tcreateNumbers,\n\tDEFAULT_ENDPOINT,\n\ttype EventBeacon,\n\ttype ExposureBeacon,\n\ttype NumbersClient,\n} from '@obelism/numbers-sdk'\n\nimport { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { getCookie, setCookie } from './utils/clientCookie'\nimport { ANALYTICS_PATH } from './config/urls'\nimport { pushDataLayer, resetDataLayerEcommerce } from './utils/pushDataLayer'\nimport {\n\tImproveAnalyticPayload,\n\tImproveEventName,\n\tImproveSetupArgs,\n} from './types'\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\n/**\n * A real-world tracked event. Test-independent — the server attributes it to a\n * test/flag at read time by joining the visitor's exposures. Sent through the\n * numbers SDK (`@obelism/numbers-sdk`), which adds the page's path and UTM tags.\n */\nexport type CreateEvent = EventBeacon\n\n/**\n * A visitor's assignment to a test/flag variant, emitted the first time the SDK\n * resolves a non-holdout value for them. The per-variant exposure count is the\n * denominator for results.\n */\nexport type CreateExposure = ExposureBeacon\n\n// Events already sent this instance, keyed by event name (or `event:dedupeKey`\n// when the caller passes one), so each fires at most once per page/session.\ntype TrackedEvents = {\n\t[event: string]: boolean\n}\n\nexport class ImproveClientSDK extends BaseImproveSDK {\n\t#visitor: Visitor | undefined\n\t#visitorId: string = ''\n\n\t#analytics: TrackedEvents = {}\n\n\t// Events and exposures go to Obelism numbers through its SDK, which keeps\n\t// the visitor cookie, the device categories, the page context, the size\n\t// and field limits, and backs off after a 429.\n\t#numbers: NumbersClient\n\n\t// Fires the missing-scope warning at most once per SDK instance, not once\n\t// per call — a caller that forgets `scope` is likely forgetting it everywhere.\n\t#warnedMissingScope = false\n\n\t#dataLayerEnabled: boolean = true\n\n\tfetchConfig = this._fetchConfig\n\n\tconstructor(args: ImproveSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#numbers = createNumbers({\n\t\t\torganizationId: this.organizationId,\n\t\t\tspaceId: this.spaceId,\n\t\t\tenvironment: this.environment,\n\t\t\t// numbers takes the events; a custom `baseUrl` (e.g. a local Improve)\n\t\t\t// keeps getting them at its own /api/log.\n\t\t\tendpoint: args.baseUrl\n\t\t\t\t? `${args.baseUrl}${ANALYTICS_PATH}`\n\t\t\t\t: DEFAULT_ENDPOINT,\n\t\t\tdisableWarnings: args.disableWarnings ?? false,\n\t\t})\n\t\tthis.#dataLayerEnabled = args.dataLayer ?? true\n\t}\n\n\tsetupVisitor = (userAgent: string = window.navigator.userAgent) => {\n\t\t// The numbers SDK reads (or makes) the visitor in the shared `visitorId`\n\t\t// cookie, so the visitor is the same one in numbers' own script.\n\t\tconst visitor = this.#numbers.visitor()\n\n\t\tif (!visitor) return null\n\n\t\tthis.#visitorId = visitor.id\n\n\t\tconst parsedUserAgent = parseUserAgent(userAgent)\n\n\t\tif (!parsedUserAgent) return null\n\n\t\tthis.#visitor = parsedUserAgent\n\n\t\t// The audiences' categories, so numbers reports the same ones.\n\t\tconst { pointer, device, browser, os } = parsedUserAgent\n\t\tthis.#numbers.setDevice({ pointer, device, browser, os })\n\n\t\treturn this.#visitorId\n\t}\n\n\tgetFlagValue = (flagSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst flagConfig = this.config.flags[flagSlug]\n\n\t\tif (!flagConfig || !flagConfig.options[0]) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\t\tif (!this.#visitorId || !this.#visitor) return flagConfig.options[0].slug\n\t\tif (this.#visitor?.[flagSlug]) return this.#visitor[flagSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience,\n\t\t// keeping the visitor sticky and honoring a server-side (middleware)\n\t\t// country-targeted assignment.\n\t\tconst cookieFlagValue = getCookie(flagSlug)\n\t\tif (\n\t\t\tcookieFlagValue &&\n\t\t\tflagConfig.options.some((option) => option.slug === cookieFlagValue)\n\t\t) {\n\t\t\tthis.#visitor[flagSlug] = cookieFlagValue\n\t\t\tthis.#postExposure('flag', flagConfig.id, cookieFlagValue)\n\t\t\treturn cookieFlagValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return flagConfig.options[0].slug\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tthis.#visitor[flagSlug] = flagValue\n\n\t\tsetCookie(flagSlug, flagValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this flag/variant server-side.\n\t\tthis.#postExposure('flag', flagConfig.id, flagValue)\n\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (testSlug: string) => {\n\t\tif (!this.config) return null\n\n\t\tconst testConfig = this.config.tests[testSlug]\n\n\t\tif (!testConfig) return null\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\tif (!this.#visitorId || !this.#visitor) return testConfig.defaultValue\n\t\tif (this.#visitor?.[testSlug]) return this.#visitor[testSlug]\n\n\t\t// Trust a prior assignment (a valid cookie) before re-evaluating audience\n\t\t// or allocation, so a bucketed visitor stays sticky. This is also how a\n\t\t// country-targeted test resolves in the browser: middleware bucketed the\n\t\t// visitor server-side (where geo is known) and set the cookie, and here we\n\t\t// honor it and record the exposure.\n\t\tconst cookieTestValue = getCookie(testSlug)\n\t\tif (cookieTestValue && this.validateTestValue(testSlug, cookieTestValue)) {\n\t\t\tthis.#visitor[testSlug] = cookieTestValue\n\t\t\tthis.#postExposure('test', testConfig.id, cookieTestValue)\n\t\t\treturn cookieTestValue\n\t\t}\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tthis.#visitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tthis.#visitor[testSlug] = testConfig.defaultValue\n\t\t\treturn this.#visitor?.[testSlug]\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tthis.#visitor[testSlug] = testValue\n\n\t\tsetCookie(testSlug, testValue)\n\n\t\t// Record the assignment so the visitor's later events can be attributed to\n\t\t// this test/variant server-side. Holdout (allocation) and audience-excluded\n\t\t// visitors return above without an exposure — they aren't in the test.\n\t\tthis.#postExposure('test', testConfig.id, testValue)\n\n\t\treturn testValue\n\t}\n\n\tsetAnalyticsUrls = (url: string) => {\n\t\tthis.#numbers.setEndpoint(url)\n\t}\n\n\t// Emits an exposure beacon the first time a visitor is assigned to a subject\n\t// this instance (the numbers SDK sends each subject once). Idempotent\n\t// server-side (unique per visitor+subject).\n\t#postExposure = (\n\t\tsubjectKind: 'test' | 'flag',\n\t\tsubjectId: string,\n\t\tvariant: string,\n\t) => {\n\t\tif (!this.#visitor || !this.#visitorId) return\n\n\t\treturn this.#numbers.expose({ subjectKind, subjectId, variant })\n\t}\n\n\tpostAnalytic = (\n\t\tevent: ImproveEventName,\n\t\t// Required, freeform, but always present — the stable second half of the\n\t\t// event's identity (e.g. \"homepage_hero_login\"). Not a display string;\n\t\t// keep it structured and consistent, unlike the old `message` field.\n\t\tscope: string,\n\t\tpayload?: ImproveAnalyticPayload,\n\t) => {\n\t\tif (!this.config) return null\n\n\t\t// GTM reserves the `gtm.*` namespace for its own events; never emit into\n\t\t// it, or the mirrored dataLayer entry would collide with GTM internals.\n\t\tif (event.startsWith('gtm.')) return null\n\n\t\t// The numbers SDK nudges toward snake_case names itself; an empty scope\n\t\t// is improve's own convention to warn about (numbers falls back to the\n\t\t// page's path).\n\t\tif (!scope && !this.#warnedMissingScope) {\n\t\t\tthis.#warnedMissingScope = true\n\t\t\tthis._warn(\n\t\t\t\t`postAnalytic(\"${event}\", ...) was called with an empty scope. ` +\n\t\t\t\t\t`scope is required — pass a short, stable identifier for where/what ` +\n\t\t\t\t\t`this event refers to, e.g. \"homepage_hero_login\".`,\n\t\t\t)\n\t\t}\n\n\t\tconst { value, currency, params, dedupeKey }: ImproveAnalyticPayload =\n\t\t\tpayload ?? {}\n\t\tconst hasValue = typeof value === 'number' && Number.isFinite(value)\n\t\tconst validParams =\n\t\t\tparams && typeof params === 'object' && !Array.isArray(params)\n\t\t\t\t? params\n\t\t\t\t: undefined\n\n\t\tif (!this.#visitor) this.setupVisitor()\n\n\t\t// The event is test-independent: it's recorded once per visitor and later\n\t\t// attributed to whichever tests/flags the visitor was exposed to. Dedup per\n\t\t// event name (or per `event:dedupeKey`, when the caller distinguishes\n\t\t// repeated firings of the same name) so it fires at most once per\n\t\t// page/session.\n\t\tconst trackingKey = dedupeKey ? `${event}:${dedupeKey}` : event\n\t\tif (!this.#visitor || this.#analytics[trackingKey]) return null\n\n\t\tconst request = this.#numbers.track(event, scope, {\n\t\t\t...(hasValue ? { value } : {}),\n\t\t\t...(currency ? { currency } : {}),\n\t\t\t...(validParams ? { params: validParams } : {}),\n\t\t})\n\n\t\t// Nothing sent (paused after a 429): don't mark the event as tracked, so\n\t\t// it can still fire once the cooldown elapses.\n\t\tif (!request) return null\n\n\t\tthis.#analytics[trackingKey] = true\n\n\t\tif (this.#dataLayerEnabled) {\n\t\t\t// Clear any prior ecommerce object first so its keys don't bleed into\n\t\t\t// this event (GTM/GA4 best practice).\n\t\t\tif (validParams && 'ecommerce' in validParams) resetDataLayerEcommerce()\n\n\t\t\tpushDataLayer({\n\t\t\t\tevent,\n\t\t\t\tscope,\n\t\t\t\timprove: {\n\t\t\t\t\tvisitorId: this.#visitorId,\n\t\t\t\t},\n\t\t\t\t...(hasValue ? { value } : {}),\n\t\t\t\t...(currency ? { currency } : {}),\n\t\t\t\t// Spread custom params (incl. a GA4 `ecommerce` object) to the top\n\t\t\t\t// level so GA4/GTM tags can read them directly.\n\t\t\t\t...validParams,\n\t\t\t\t_improve: true,\n\t\t\t})\n\t\t}\n\n\t\treturn request\n\t}\n}\n","import { ImproveEventName } from '../types'\n\nexport type ImproveDataLayerEntry = {\n\tevent: ImproveEventName\n\timprove: {\n\t\t// Events are test-independent; attribution to a test/variant happens\n\t\t// server-side by joining the visitor's exposures, so only the visitor is\n\t\t// carried here.\n\t\tvisitorId: string\n\t}\n\t/** GA4 numeric value / currency, when provided on the analytic. */\n\tvalue?: number\n\tcurrency?: string\n\t/**\n\t * Marks the entry as originating from Improve so platform-side dataLayer\n\t * importers can ignore it (loop prevention).\n\t */\n\t_improve: true\n\t/** Any extra event params (e.g. a GA4 `ecommerce` object) spread onto the entry. */\n\t[param: string]: unknown\n}\n\ndeclare global {\n\tinterface Window {\n\t\tdataLayer?: Record<string, unknown>[]\n\t}\n}\n\n/**\n * Mirror an analytic onto the GTM dataLayer (with experiment dimensions) so it\n * can drive Google Tag Manager / Google Ads conversions. No-op outside the\n * browser. Initializes window.dataLayer if GTM hasn't yet.\n */\nexport const pushDataLayer = (entry: ImproveDataLayerEntry) => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push(entry)\n}\n\n/**\n * Clear the dataLayer `ecommerce` object before pushing an ecommerce event, so\n * leftover keys (old `items`, a stale `coupon`…) from a previous event don't\n * bleed into the next one — the GTM/GA4 recommended reset. No-op outside the\n * browser.\n */\nexport const resetDataLayerEcommerce = () => {\n\tif (typeof window === 'undefined') return\n\n\twindow.dataLayer = window.dataLayer || []\n\twindow.dataLayer.push({ ecommerce: null })\n}\n"],"names":["getCookie","name","cookie","document","split","find","row","key","setCookie","value","now","Date","setDate","getDate","toUTCString","ImproveClientSDK","BaseImproveSDK","args","fetchConfig","_fetchConfig","setupVisitor","userAgent","window","navigator","visitor","id","parsedUserAgent","parseUserAgent","pointer","device","browser","os","setDevice","getFlagValue","flagSlug","config","flagConfig","flags","options","slug","cookieFlagValue","some","option","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testSlug","testConfig","tests","defaultValue","cookieTestValue","validateTestValue","allocation","Math","random","testValue","setAnalyticsUrls","url","setEndpoint","subjectKind","subjectId","variant","expose","postAnalytic","event","scope","payload","startsWith","_warn","currency","params","dedupeKey","hasValue","Number","isFinite","validParams","Array","isArray","undefined","trackingKey","request","track","entry","dataLayer","push","ecommerce","improve","visitorId","_improve","createNumbers","organizationId","spaceId","environment","endpoint","baseUrl","ANALYTICS_PATH","DEFAULT_ENDPOINT","disableWarnings"],"mappings":"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"}
package/dist/oidc.cjs ADDED
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,"__esModule",{value:!0});let e="https://improve.obelism.studio";exports.DEFAULT_AUDIENCE=e,exports.githubOidc=({audience:t=e,fetch:r=fetch}={})=>{var o;let i,s,a;return o=async()=>{let e=globalThis.process?.env,o=e?.ACTIONS_ID_TOKEN_REQUEST_URL,i=e?.ACTIONS_ID_TOKEN_REQUEST_TOKEN;if(!o||!i)throw Error("githubOidc needs a GitHub Actions job with `permissions: id-token: write`");let s=await r(`${o}&audience=${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${i}`}});if(!s.ok)throw Error(`GitHub refused the OIDC token request (${s.status})`);return(await s.json()).value},i="",s=0,a=null,()=>i&&Date.now()<s-6e4?Promise.resolve(i):a??=o().then(e=>(i=e,s=(e=>{try{let t=e.split(".")[1].replace(/-/g,"+").replace(/_/g,"/");return 1e3*JSON.parse(atob(t)).exp}catch{return 0}})(e),e)).finally(()=>{a=null})},exports.vercelOidc=({audience:t=e}={})=>async()=>{let{getVercelOidcToken:e}=await import("@vercel/oidc");return e({audience:t,expirationBufferMs:6e4})};
2
+ //# sourceMappingURL=oidc.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.cjs","sources":["../src/oidc.ts"],"sourcesContent":["// A deployment's host OIDC token instead of a stored API token\n// (Obelism's www docs/adr/0016): pass one of these as `token`. The space\n// needs the Vercel project or GitHub repository connected, under Connected\n// deployments on its page on www.obelism.studio. Same as Content's packages/content/src/oidc.ts.\n//\n// import { vercelOidc } from '@obelism/improve-sdk/oidc'\n// token: vercelOidc()\n\nexport type { TokenSource } from './tokenSource'\n\n// What the token must be addressed to: this product.\nexport const DEFAULT_AUDIENCE = 'https://improve.obelism.studio'\n\n// Renewed this long before it expires, so a request never carries one that\n// expires on the way.\nconst RENEW_MARGIN_MS = 60_000\n\nconst expiryOf = (token: string) => {\n\ttry {\n\t\tconst payload = token.split('.')[1]!.replace(/-/g, '+').replace(/_/g, '/')\n\t\treturn (JSON.parse(atob(payload)) as { exp?: number }).exp! * 1000\n\t} catch {\n\t\treturn 0\n\t}\n}\n\n// One token at a time, reused until shortly before it expires; concurrent\n// callers share the request for a new one. For GitHub's, which nothing\n// else caches.\nconst reused = (fetchToken: () => Promise<string>) => {\n\tlet token = ''\n\tlet expiresAt = 0\n\tlet pending: Promise<string> | null = null\n\treturn () => {\n\t\tif (token && Date.now() < expiresAt - RENEW_MARGIN_MS)\n\t\t\treturn Promise.resolve(token)\n\t\tpending ??= fetchToken()\n\t\t\t.then((fresh) => {\n\t\t\t\ttoken = fresh\n\t\t\t\texpiresAt = expiryOf(fresh)\n\t\t\t\treturn fresh\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tpending = null\n\t\t\t})\n\t\treturn pending\n\t}\n}\n\n// On Vercel: the deployment's token for this product, from @vercel/oidc\n// (install it next to this package), which caches the exchange for the\n// audience itself. In a function it reads the request's token, so call it\n// during a request, not at module level.\nexport const vercelOidc =\n\t({ audience = DEFAULT_AUDIENCE }: { audience?: string | undefined } = {}) =>\n\tasync () => {\n\t\tconst { getVercelOidcToken } = await import('@vercel/oidc')\n\t\treturn getVercelOidcToken({\n\t\t\taudience,\n\t\t\texpirationBufferMs: RENEW_MARGIN_MS,\n\t\t})\n\t}\n\n// In GitHub Actions: the job's token for this product. The job needs\n// `permissions: id-token: write`.\nexport const githubOidc = ({\n\taudience = DEFAULT_AUDIENCE,\n\tfetch: fetchImpl = fetch,\n}: {\n\taudience?: string | undefined\n\tfetch?: typeof fetch | undefined\n} = {}) =>\n\treused(async () => {\n\t\tconst env = (\n\t\t\tglobalThis as { process?: { env: Record<string, string | undefined> } }\n\t\t).process?.env\n\t\tconst url = env?.ACTIONS_ID_TOKEN_REQUEST_URL\n\t\tconst bearer = env?.ACTIONS_ID_TOKEN_REQUEST_TOKEN\n\t\tif (!url || !bearer)\n\t\t\tthrow new Error(\n\t\t\t\t'githubOidc needs a GitHub Actions job with `permissions: id-token: write`',\n\t\t\t)\n\t\tconst response = await fetchImpl(\n\t\t\t`${url}&audience=${encodeURIComponent(audience)}`,\n\t\t\t{ headers: { Authorization: `Bearer ${bearer}` } },\n\t\t)\n\t\tif (!response.ok)\n\t\t\tthrow new Error(\n\t\t\t\t`GitHub refused the OIDC token request (${response.status})`,\n\t\t\t)\n\t\treturn ((await response.json()) as { value: string }).value\n\t})\n"],"names":["DEFAULT_AUDIENCE","audience","fetch","fetchImpl","fetchToken","token","expiresAt","pending","env","globalThis","process","url","ACTIONS_ID_TOKEN_REQUEST_URL","bearer","ACTIONS_ID_TOKEN_REQUEST_TOKEN","Error","response","encodeURIComponent","headers","Authorization","ok","status","json","value","Date","now","Promise","resolve","then","fresh","expiryOf","payload","split","replace","JSON","parse","atob","exp","finally","getVercelOidcToken","expirationBufferMs"],"mappings":"uDAWO,IAAMA,EAAmB,+EAsDN,CAAC,CAC1BC,SAAAA,EAAWD,CAAgB,CAC3BE,MAAOC,EAAYD,KAAK,CAIxB,CAAG,EAAE,QA1CUE,MACXC,EACAC,EACAC,SAHWH,EA2CR,UACN,IAAMI,EACLC,WACCC,OAAO,EAAEF,IACLG,EAAMH,GAAKI,6BACXC,EAASL,GAAKM,+BACpB,GAAI,CAACH,GAAO,CAACE,EACZ,MAAM,AAAIE,MACT,6EAEF,IAAMC,EAAW,MAAMb,EACtB,CAAA,EAAGQ,EAAI,UAAU,EAAEM,mBAAmBhB,GAAAA,CAAW,CACjD,CAAEiB,QAAS,CAAEC,cAAe,CAAC,OAAO,EAAEN,EAAAA,CAAAA,AAAS,CAAE,GAElD,GAAI,CAACG,EAASI,EAAE,CACf,MAAM,AAAIL,MACT,CAAC,uCAAuC,EAAEC,EAASK,MAAM,CAAC,CAAC,CAAC,EAE9D,MAAO,AAAE,CAAA,MAAML,EAASM,IAAI,EAAA,EAA0BC,KAAK,AAC5D,EA7DIlB,EAAQ,GACRC,EAAY,EACZC,EAAkC,KAC/B,IACN,AAAIF,GAASmB,KAAKC,GAAG,GAAKnB,EAnBJ,IAoBdoB,QAAQC,OAAO,CAACtB,GACxBE,IAAYH,IACVwB,IAAI,CAAC,AAACC,IACNxB,EAAQwB,EACRvB,EAAYwB,AAtBC,CAAA,AAACzB,IACjB,GAAI,CACH,IAAM0B,EAAU1B,EAAM2B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAEC,OAAO,CAAC,KAAM,KAAKA,OAAO,CAAC,KAAM,KACtE,OAAOC,AAAuD,IAAvDA,KAAMC,KAAK,CAACC,KAAKL,IAA+BM,GAAG,AAC3D,CAAE,KAAM,CACP,OAAO,CACR,CACD,CAAA,EAeyBR,GACdA,IAEPS,OAAO,CAAC,KACR/B,EAAU,IACX,uBAUF,CAAC,CAAEN,SAAAA,EAAWD,CAAgB,CAAqC,CAAG,CAAA,CAAE,GACxE,UACC,GAAM,CAAEuC,mBAAAA,CAAkB,CAAE,CAAG,MAAM,MAAA,CAAO,gBAC5C,OAAOA,EAAmB,CACzBtC,SAAAA,EACAuC,mBA5CqB,GA6CtB,EACD"}
package/dist/oidc.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ export { T as TokenSource } from './tokenSource-my2ymemb.js';
2
+
3
+ declare const DEFAULT_AUDIENCE = "https://improve.obelism.studio";
4
+ declare const vercelOidc: ({ audience }?: {
5
+ audience?: string | undefined;
6
+ }) => () => Promise<string>;
7
+ declare const githubOidc: ({ audience, fetch: fetchImpl, }?: {
8
+ audience?: string | undefined;
9
+ fetch?: typeof fetch | undefined;
10
+ }) => () => Promise<string>;
11
+
12
+ export { DEFAULT_AUDIENCE, githubOidc, vercelOidc };
13
+ //# sourceMappingURL=oidc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.d.ts","sources":["../src/oidc.ts"],"mappings":";;AACA,cAAc,gBAAgB;AAC9B,cAAc,UAAU;AACxB;AACA,YAAY,OAAO;AACnB,cAAc,UAAU;AACxB;AACA,mBAAmB,KAAK;AACxB,YAAY,OAAO;;","names":[]}
package/dist/oidc.mjs ADDED
@@ -0,0 +1,2 @@
1
+ let e="https://improve.obelism.studio",t=({audience:t=e}={})=>async()=>{let{getVercelOidcToken:e}=await import("@vercel/oidc");return e({audience:t,expirationBufferMs:6e4})},r=({audience:t=e,fetch:r=fetch}={})=>{var i;let o,a,n;return i=async()=>{let e=globalThis.process?.env,i=e?.ACTIONS_ID_TOKEN_REQUEST_URL,o=e?.ACTIONS_ID_TOKEN_REQUEST_TOKEN;if(!i||!o)throw Error("githubOidc needs a GitHub Actions job with `permissions: id-token: write`");let a=await r(`${i}&audience=${encodeURIComponent(t)}`,{headers:{Authorization:`Bearer ${o}`}});if(!a.ok)throw Error(`GitHub refused the OIDC token request (${a.status})`);return(await a.json()).value},o="",a=0,n=null,()=>o&&Date.now()<a-6e4?Promise.resolve(o):n??=i().then(e=>(o=e,a=(e=>{try{let t=e.split(".")[1].replace(/-/g,"+").replace(/_/g,"/");return 1e3*JSON.parse(atob(t)).exp}catch{return 0}})(e),e)).finally(()=>{n=null})};export{e as DEFAULT_AUDIENCE,r as githubOidc,t as vercelOidc};
2
+ //# sourceMappingURL=oidc.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oidc.mjs","sources":["../src/oidc.ts"],"sourcesContent":["// A deployment's host OIDC token instead of a stored API token\n// (Obelism's www docs/adr/0016): pass one of these as `token`. The space\n// needs the Vercel project or GitHub repository connected, under Connected\n// deployments on its page on www.obelism.studio. Same as Content's packages/content/src/oidc.ts.\n//\n// import { vercelOidc } from '@obelism/improve-sdk/oidc'\n// token: vercelOidc()\n\nexport type { TokenSource } from './tokenSource'\n\n// What the token must be addressed to: this product.\nexport const DEFAULT_AUDIENCE = 'https://improve.obelism.studio'\n\n// Renewed this long before it expires, so a request never carries one that\n// expires on the way.\nconst RENEW_MARGIN_MS = 60_000\n\nconst expiryOf = (token: string) => {\n\ttry {\n\t\tconst payload = token.split('.')[1]!.replace(/-/g, '+').replace(/_/g, '/')\n\t\treturn (JSON.parse(atob(payload)) as { exp?: number }).exp! * 1000\n\t} catch {\n\t\treturn 0\n\t}\n}\n\n// One token at a time, reused until shortly before it expires; concurrent\n// callers share the request for a new one. For GitHub's, which nothing\n// else caches.\nconst reused = (fetchToken: () => Promise<string>) => {\n\tlet token = ''\n\tlet expiresAt = 0\n\tlet pending: Promise<string> | null = null\n\treturn () => {\n\t\tif (token && Date.now() < expiresAt - RENEW_MARGIN_MS)\n\t\t\treturn Promise.resolve(token)\n\t\tpending ??= fetchToken()\n\t\t\t.then((fresh) => {\n\t\t\t\ttoken = fresh\n\t\t\t\texpiresAt = expiryOf(fresh)\n\t\t\t\treturn fresh\n\t\t\t})\n\t\t\t.finally(() => {\n\t\t\t\tpending = null\n\t\t\t})\n\t\treturn pending\n\t}\n}\n\n// On Vercel: the deployment's token for this product, from @vercel/oidc\n// (install it next to this package), which caches the exchange for the\n// audience itself. In a function it reads the request's token, so call it\n// during a request, not at module level.\nexport const vercelOidc =\n\t({ audience = DEFAULT_AUDIENCE }: { audience?: string | undefined } = {}) =>\n\tasync () => {\n\t\tconst { getVercelOidcToken } = await import('@vercel/oidc')\n\t\treturn getVercelOidcToken({\n\t\t\taudience,\n\t\t\texpirationBufferMs: RENEW_MARGIN_MS,\n\t\t})\n\t}\n\n// In GitHub Actions: the job's token for this product. The job needs\n// `permissions: id-token: write`.\nexport const githubOidc = ({\n\taudience = DEFAULT_AUDIENCE,\n\tfetch: fetchImpl = fetch,\n}: {\n\taudience?: string | undefined\n\tfetch?: typeof fetch | undefined\n} = {}) =>\n\treused(async () => {\n\t\tconst env = (\n\t\t\tglobalThis as { process?: { env: Record<string, string | undefined> } }\n\t\t).process?.env\n\t\tconst url = env?.ACTIONS_ID_TOKEN_REQUEST_URL\n\t\tconst bearer = env?.ACTIONS_ID_TOKEN_REQUEST_TOKEN\n\t\tif (!url || !bearer)\n\t\t\tthrow new Error(\n\t\t\t\t'githubOidc needs a GitHub Actions job with `permissions: id-token: write`',\n\t\t\t)\n\t\tconst response = await fetchImpl(\n\t\t\t`${url}&audience=${encodeURIComponent(audience)}`,\n\t\t\t{ headers: { Authorization: `Bearer ${bearer}` } },\n\t\t)\n\t\tif (!response.ok)\n\t\t\tthrow new Error(\n\t\t\t\t`GitHub refused the OIDC token request (${response.status})`,\n\t\t\t)\n\t\treturn ((await response.json()) as { value: string }).value\n\t})\n"],"names":["DEFAULT_AUDIENCE","vercelOidc","audience","getVercelOidcToken","expirationBufferMs","githubOidc","fetch","fetchImpl","fetchToken","token","expiresAt","pending","env","globalThis","process","url","ACTIONS_ID_TOKEN_REQUEST_URL","bearer","ACTIONS_ID_TOKEN_REQUEST_TOKEN","Error","response","encodeURIComponent","headers","Authorization","ok","status","json","value","Date","now","Promise","resolve","then","fresh","expiryOf","payload","split","replace","JSON","parse","atob","exp","finally"],"mappings":"AAWO,IAAMA,EAAmB,iCA0CnBC,EACZ,CAAC,CAAEC,SAAAA,EAAWF,CAAgB,CAAqC,CAAG,CAAA,CAAE,GACxE,UACC,GAAM,CAAEG,mBAAAA,CAAkB,CAAE,CAAG,MAAM,MAAA,CAAO,gBAC5C,OAAOA,EAAmB,CACzBD,SAAAA,EACAE,mBA5CqB,GA6CtB,EACD,EAIYC,EAAa,CAAC,CAC1BH,SAAAA,EAAWF,CAAgB,CAC3BM,MAAOC,EAAYD,KAAK,CAIxB,CAAG,EAAE,QA1CUE,MACXC,EACAC,EACAC,SAHWH,EA2CR,UACN,IAAMI,EACLC,WACCC,OAAO,EAAEF,IACLG,EAAMH,GAAKI,6BACXC,EAASL,GAAKM,+BACpB,GAAI,CAACH,GAAO,CAACE,EACZ,MAAM,AAAIE,MACT,6EAEF,IAAMC,EAAW,MAAMb,EACtB,CAAA,EAAGQ,EAAI,UAAU,EAAEM,mBAAmBnB,GAAAA,CAAW,CACjD,CAAEoB,QAAS,CAAEC,cAAe,CAAC,OAAO,EAAEN,EAAAA,CAAAA,AAAS,CAAE,GAElD,GAAI,CAACG,EAASI,EAAE,CACf,MAAM,AAAIL,MACT,CAAC,uCAAuC,EAAEC,EAASK,MAAM,CAAC,CAAC,CAAC,EAE9D,MAAO,AAAE,CAAA,MAAML,EAASM,IAAI,EAAA,EAA0BC,KAAK,AAC5D,EA7DIlB,EAAQ,GACRC,EAAY,EACZC,EAAkC,KAC/B,IACN,AAAIF,GAASmB,KAAKC,GAAG,GAAKnB,EAnBJ,IAoBdoB,QAAQC,OAAO,CAACtB,GACxBE,IAAYH,IACVwB,IAAI,CAAC,AAACC,IACNxB,EAAQwB,EACRvB,EAAYwB,AAtBC,CAAA,AAACzB,IACjB,GAAI,CACH,IAAM0B,EAAU1B,EAAM2B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAEC,OAAO,CAAC,KAAM,KAAKA,OAAO,CAAC,KAAM,KACtE,OAAOC,AAAuD,IAAvDA,KAAMC,KAAK,CAACC,KAAKL,IAA+BM,GAAG,AAC3D,CAAE,KAAM,CACP,OAAO,CACR,CACD,CAAA,EAeyBR,GACdA,IAEPS,OAAO,CAAC,KACR/B,EAAU,IACX"}
package/dist/server.cjs CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,"__esModule",{value:!0});var t=require("./base-gi7y83ve.js");class e extends t.BaseImproveSDK{#t;#e;#i;constructor({token:e,maxVisitors:i,...s}){super(s),this.#t=new Map,this.fetchConfig=async t=>this._fetchConfig({...t,headers:{...t?.headers,token:this.#i}}),this.getFlagConfig=t=>this.config?.flags?.[t],this.getTestConfig=t=>this.config?.tests?.[t],this.#s=(e,i)=>{let s=this.#t.get(e);if(s)this.#t.delete(e),this.#t.set(e,s);else{if(this.#t.size>=this.#e){let t=this.#t.keys().next().value;t&&this.#t.delete(t)}s={},this.#t.set(e,s)}let r=s[i]||t.parseUserAgent(i);return r?(s[i]=r,r):null},this.getFlagValue=(e,i,s,r)=>{let n=this.getFlagConfig(e);if(!n||!this.config)return null;let o=n.options[0]?.slug??null;if(!i)return o;let l=this.#s(i,s);if(!l)return o;r?.country&&(l.country=r.country);let u=l[e];if(u)return u;if(!t.getVisitorMatchesAudience(this.config.audience[n.audience],l))return o;let a=t.getRandomTestValue(n.options);return a?(l[e]=a,a):null},this.getTestValue=(e,i,s,r)=>{let n=this.getTestConfig(e);if(!n||!this.config)return null;if(!i||!s)return n.defaultValue;let o=this.#s(i,s);if(!o)return n.defaultValue;r?.country&&(o.country=r.country);let l=o[e];if(l)return l;if(!t.getVisitorMatchesAudience(this.config.audience[n.audience],o))return n.defaultValue;if(n.allocation<100&&100*Math.random()>n.allocation)return o[e]=n.defaultValue,n.defaultValue;let u=t.getRandomTestValue(n.options);return u?(o[e]=u,u):null},this.#i=e,this.#e=i??1e4}#s}exports.ImproveServerSDK=e;
1
+ Object.defineProperty(exports,"__esModule",{value:!0});var t=require("./base-8q51gsqe.js");let e=(t,e)=>Object.hasOwn(t,e)?t[e]:void 0;class i extends t.BaseImproveSDK{#t;#e;#i;constructor({token:i,maxVisitors:s,...r}){super(r),this.#t=new Map,this.fetchConfig=async t=>{let e;return this._fetchConfig({...t,headers:{...t?.headers,token:await ("function"==typeof(e=this.#i)?e():e)}})},this.getFlagConfig=t=>this.config?.flags?.[t],this.getTestConfig=t=>this.config?.tests?.[t],this.#s=(e,i)=>{let s=this.#t.get(e);if(s)this.#t.delete(e),this.#t.set(e,s);else{if(this.#t.size>=this.#e){let t=this.#t.keys().next().value;t&&this.#t.delete(t)}s=new Map,this.#t.set(e,s)}let r=s.get(i);if(r)return r;let n=t.parseUserAgent(i);return n?(s.size>=10&&s.delete(s.keys().next().value),s.set(i,n),n):null},this.getFlagValue=(i,s,r,n)=>{let o=this.getFlagConfig(i);if(!o||!this.config)return null;let l=o.options[0]?.slug??null;if(!s)return l;let u=this.#s(s,r);if(!u)return l;n?.country&&(u.country=n.country);let a=e(u,i);if(a)return a;if(!t.getVisitorMatchesAudience(this.config.audience[o.audience],u))return l;let f=t.getRandomTestValue(o.options);return f?(u[i]=f,f):null},this.getTestValue=(i,s,r,n)=>{let o=this.getTestConfig(i);if(!o||!this.config)return null;if(!s||!r)return o.defaultValue;let l=this.#s(s,r);if(!l)return o.defaultValue;n?.country&&(l.country=n.country);let u=e(l,i);if(u)return u;if(!t.getVisitorMatchesAudience(this.config.audience[o.audience],l))return o.defaultValue;if(o.allocation<100&&100*Math.random()>o.allocation)return l[i]=o.defaultValue,o.defaultValue;let a=t.getRandomTestValue(o.options);return a?(l[i]=a,a):null},this.#i=i,this.#e=s??1e4}#s}exports.ImproveServerSDK=i;
2
2
  //# sourceMappingURL=server.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.cjs","sources":["../src/server.ts"],"sourcesContent":["import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\ntype VisitorData = {\n\t[userAgent: string]: Visitor\n}\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\ttoken: string\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: string\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: this.#token,\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t// The visitor as seen with this user agent, or null when it can't be\n\t// parsed (an empty user agent), which gets the default like a holdout.\n\t#getVisitor = (visitorId: string, userAgent: string): Visitor | null => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = {}\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tconst parsed = visitor[userAgent] || parseUserAgent(userAgent)\n\t\tif (!parsed) return null\n\t\tvisitor[userAgent] = parsed\n\t\treturn parsed\n\t}\n\n\tgetFlagValue = (\n\t\tflagSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\n\t\tconst defaultValue = flagConfig.options[0]?.slug ?? null\n\t\tif (!visitorId) return defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = visitor[flagSlug]\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return defaultValue\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (\n\t\ttestSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return testConfig.defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = visitor[testSlug]\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[testSlug] = testConfig.defaultValue\n\t\t\treturn testConfig.defaultValue\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n"],"names":["ImproveServerSDK","BaseImproveSDK","token","maxVisitors","args","Map","fetchConfig","config","_fetchConfig","headers","getFlagConfig","flagSlug","flags","getTestConfig","testSlug","tests","visitorId","userAgent","visitor","get","delete","set","size","oldest","keys","next","value","parsed","parseUserAgent","getFlagValue","geo","flagConfig","defaultValue","options","slug","country","assigned","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testConfig","allocation","Math","random","testValue"],"mappings":"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"}
1
+ {"version":3,"file":"server.cjs","sources":["../src/server.ts","../src/tokenSource.ts"],"sourcesContent":["import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { type TokenSource, resolveToken } from './tokenSource'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n// One visitor id with endless user agents would otherwise grow without bound.\nconst MAX_USER_AGENTS_PER_VISITOR = 10\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\n// A Map, not an object: the user agent is the request's, and `__proto__`\n// would be Object.prototype.\ntype VisitorData = Map<string, Visitor>\n\nconst assignment = (visitor: Visitor, slug: string) =>\n\tObject.hasOwn(visitor, slug) ? visitor[slug] : undefined\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\t// An API token (`imp_…`), or a function giving one per request,\n\t\t\t// like `vercelOidc()` from `@obelism/improve-sdk/oidc`.\n\t\t\ttoken: TokenSource\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: TokenSource\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: await resolveToken(this.#token),\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t// The visitor as seen with this user agent, or null when it can't be\n\t// parsed (an empty user agent), which gets the default like a holdout.\n\t#getVisitor = (visitorId: string, userAgent: string): Visitor | null => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = new Map()\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tconst known = visitor.get(userAgent)\n\t\tif (known) return known\n\t\tconst parsed = parseUserAgent(userAgent)\n\t\tif (!parsed) return null\n\t\tif (visitor.size >= MAX_USER_AGENTS_PER_VISITOR) {\n\t\t\tvisitor.delete(visitor.keys().next().value!)\n\t\t}\n\t\tvisitor.set(userAgent, parsed)\n\t\treturn parsed\n\t}\n\n\tgetFlagValue = (\n\t\tflagSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\n\t\tconst defaultValue = flagConfig.options[0]?.slug ?? null\n\t\tif (!visitorId) return defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = assignment(visitor, flagSlug)\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return defaultValue\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (\n\t\ttestSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return testConfig.defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = assignment(visitor, testSlug)\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[testSlug] = testConfig.defaultValue\n\t\t\treturn testConfig.defaultValue\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n","// A token, or a function that gives one per request: a host OIDC token from\n// ./oidc, or anything else that rotates.\nexport type TokenSource = string | (() => string | Promise<string>)\n\nexport const resolveToken = (token: TokenSource) =>\n\ttypeof token === 'function' ? token() : token\n"],"names":["assignment","visitor","slug","Object","hasOwn","undefined","ImproveServerSDK","BaseImproveSDK","token","maxVisitors","args","Map","fetchConfig","config","_fetchConfig","headers","getFlagConfig","flagSlug","flags","getTestConfig","testSlug","tests","visitorId","userAgent","get","delete","set","size","oldest","keys","next","value","known","parsed","parseUserAgent","getFlagValue","geo","flagConfig","defaultValue","options","country","assigned","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testConfig","allocation","Math","random","testValue"],"mappings":"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"}
package/dist/server.d.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import { BaseImproveSDK, ImproveSetupArgs, ImproveConfiguration, ImproveFlag, ImproveTest } from './types.js';
2
+ import { TokenSource } from './oidc.js';
2
3
 
3
4
  type ImproveServerSetupArgs = (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {
4
5
  config: ImproveConfiguration;
5
6
  maxVisitors?: number;
6
7
  }) | (Omit<ImproveSetupArgs, 'config'> & {
7
- token: string;
8
+ token: TokenSource;
8
9
  maxVisitors?: number;
9
10
  });
10
11
  declare class ImproveServerSDK extends BaseImproveSDK {
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sources":["../src/server.ts"],"mappings":";;AAGA,KAAK,sBAAsB,IAAI,IAAI,CAAC,gBAAgB;AACpD,YAAY,oBAAoB;AAChC;AACA,MAAM,IAAI,CAAC,gBAAgB;AAC3B;AACA;AACA;AACA,cAAc,gBAAgB,SAAS,cAAc;AACrD;AACA,iDAAiD,sBAAsB;AACvE,2BAA2B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACvE,yCAAyCA,WAAmB;AAC5D,yCAAyCC,WAAmB;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA;;","names":["__types.ImproveFlag","__types.ImproveTest"]}
1
+ {"version":3,"file":"server.d.ts","sources":["../src/server.ts"],"mappings":";;;AAIA,KAAK,sBAAsB,IAAI,IAAI,CAAC,gBAAgB;AACpD,YAAY,oBAAoB;AAChC;AACA,MAAM,IAAI,CAAC,gBAAgB;AAC3B,WAAW,WAAW;AACtB;AACA;AACA,cAAc,gBAAgB,SAAS,cAAc;AACrD;AACA,iDAAiD,sBAAsB;AACvE,2BAA2B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACvE,yCAAyCA,WAAmB;AAC5D,yCAAyCC,WAAmB;AAC5D;AACA;AACA;AACA;AACA;AACA;AACA","names":["__types.ImproveFlag","__types.ImproveTest"]}
package/dist/server.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{B as t,p as i,g as e,a as s}from"./base-i6scrpy2.mjs";class r extends t{#t;#i;#e;constructor({token:t,maxVisitors:r,...n}){super(n),this.#t=new Map,this.fetchConfig=async t=>this._fetchConfig({...t,headers:{...t?.headers,token:this.#e}}),this.getFlagConfig=t=>this.config?.flags?.[t],this.getTestConfig=t=>this.config?.tests?.[t],this.#s=(t,e)=>{let s=this.#t.get(t);if(s)this.#t.delete(t),this.#t.set(t,s);else{if(this.#t.size>=this.#i){let t=this.#t.keys().next().value;t&&this.#t.delete(t)}s={},this.#t.set(t,s)}let r=s[e]||i(e);return r?(s[e]=r,r):null},this.getFlagValue=(t,i,r,n)=>{let o=this.getFlagConfig(t);if(!o||!this.config)return null;let l=o.options[0]?.slug??null;if(!i)return l;let u=this.#s(i,r);if(!u)return l;n?.country&&(u.country=n.country);let a=u[t];if(a)return a;if(!e(this.config.audience[o.audience],u))return l;let f=s(o.options);return f?(u[t]=f,f):null},this.getTestValue=(t,i,r,n)=>{let o=this.getTestConfig(t);if(!o||!this.config)return null;if(!i||!r)return o.defaultValue;let l=this.#s(i,r);if(!l)return o.defaultValue;n?.country&&(l.country=n.country);let u=l[t];if(u)return u;if(!e(this.config.audience[o.audience],l))return o.defaultValue;if(o.allocation<100&&100*Math.random()>o.allocation)return l[t]=o.defaultValue,o.defaultValue;let a=s(o.options);return a?(l[t]=a,a):null},this.#e=t,this.#i=r??1e4}#s}export{r as ImproveServerSDK};
1
+ import{B as t,p as e,g as i,a as s}from"./base-e6yz3dpl.mjs";let r=(t,e)=>Object.hasOwn(t,e)?t[e]:void 0;class n extends t{#t;#e;#i;constructor({token:t,maxVisitors:n,...o}){super(o),this.#t=new Map,this.fetchConfig=async t=>{let e;return this._fetchConfig({...t,headers:{...t?.headers,token:await ("function"==typeof(e=this.#i)?e():e)}})},this.getFlagConfig=t=>this.config?.flags?.[t],this.getTestConfig=t=>this.config?.tests?.[t],this.#s=(t,i)=>{let s=this.#t.get(t);if(s)this.#t.delete(t),this.#t.set(t,s);else{if(this.#t.size>=this.#e){let t=this.#t.keys().next().value;t&&this.#t.delete(t)}s=new Map,this.#t.set(t,s)}let r=s.get(i);if(r)return r;let n=e(i);return n?(s.size>=10&&s.delete(s.keys().next().value),s.set(i,n),n):null},this.getFlagValue=(t,e,n,o)=>{let l=this.getFlagConfig(t);if(!l||!this.config)return null;let u=l.options[0]?.slug??null;if(!e)return u;let a=this.#s(e,n);if(!a)return u;o?.country&&(a.country=o.country);let f=r(a,t);if(f)return f;if(!i(this.config.audience[l.audience],a))return u;let h=s(l.options);return h?(a[t]=h,h):null},this.getTestValue=(t,e,n,o)=>{let l=this.getTestConfig(t);if(!l||!this.config)return null;if(!e||!n)return l.defaultValue;let u=this.#s(e,n);if(!u)return l.defaultValue;o?.country&&(u.country=o.country);let a=r(u,t);if(a)return a;if(!i(this.config.audience[l.audience],u))return l.defaultValue;if(l.allocation<100&&100*Math.random()>l.allocation)return u[t]=l.defaultValue,l.defaultValue;let f=s(l.options);return f?(u[t]=f,f):null},this.#i=t,this.#e=n??1e4}#s}export{n as ImproveServerSDK};
2
2
  //# sourceMappingURL=server.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.mjs","sources":["../src/server.ts"],"sourcesContent":["import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\ntype VisitorData = {\n\t[userAgent: string]: Visitor\n}\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\ttoken: string\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: string\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: this.#token,\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t// The visitor as seen with this user agent, or null when it can't be\n\t// parsed (an empty user agent), which gets the default like a holdout.\n\t#getVisitor = (visitorId: string, userAgent: string): Visitor | null => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = {}\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tconst parsed = visitor[userAgent] || parseUserAgent(userAgent)\n\t\tif (!parsed) return null\n\t\tvisitor[userAgent] = parsed\n\t\treturn parsed\n\t}\n\n\tgetFlagValue = (\n\t\tflagSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\n\t\tconst defaultValue = flagConfig.options[0]?.slug ?? null\n\t\tif (!visitorId) return defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = visitor[flagSlug]\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return defaultValue\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (\n\t\ttestSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return testConfig.defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = visitor[testSlug]\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[testSlug] = testConfig.defaultValue\n\t\t\treturn testConfig.defaultValue\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n"],"names":["ImproveServerSDK","BaseImproveSDK","token","maxVisitors","args","Map","fetchConfig","config","_fetchConfig","headers","getFlagConfig","flagSlug","flags","getTestConfig","testSlug","tests","visitorId","userAgent","visitor","get","delete","set","size","oldest","keys","next","value","parsed","parseUserAgent","getFlagValue","geo","flagConfig","defaultValue","options","slug","country","assigned","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testConfig","allocation","Math","random","testValue"],"mappings":"4DA0BO,OAAMA,UAAyBC,EACrC,CAAA,CAAS,AAAA,AACT,EAAA,CAAY,AAAA,AACZ,EAAA,CAAM,AAAA,AAGN,aAAY,CAAEC,MAAAA,CAAK,CAAEC,YAAAA,CAAW,CAAE,GAAGC,EAA8B,CAAE,CACpE,KAAK,CAACA,QANP,CAAA,CAAS,CAA6B,IAAIC,IAAAA,IAAAA,CAW1CC,YAAc,MAAOC,GACb,IAAI,CAACC,YAAY,CAAC,CACxB,GAAGD,CAAM,CACTE,QAAS,CACR,GAAGF,GAAQE,OAAO,CAClBP,MAAO,IAAI,CAAC,CAAA,CAAA,AACb,CACD,GACD,IAAA,CAEAQ,aAAAA,CAAgB,AAACC,GAAqB,IAAI,CAACJ,MAAM,EAAEK,OAAAA,CAAQD,EAAS,CAAA,IAAA,CAEpEE,cAAgB,AAACC,GAAqB,IAAI,CAACP,MAAM,EAAEQ,OAAAA,CAAQD,EAAS,MAIpE,CAAA,CAAW,CAAG,CAACE,EAAmBC,KACjC,IAAIC,EAAU,IAAI,CAAC,CAAA,CAAS,CAACC,GAAG,CAACH,GACjC,GAAIE,EAEH,IAAI,CAAC,CAAA,CAAS,CAACE,MAAM,CAACJ,GACtB,IAAI,CAAC,CAAA,CAAS,CAACK,GAAG,CAACL,EAAWE,OACxB,CAEN,GAAI,IAAI,CAAC,CAAA,CAAS,CAACI,IAAI,EAAI,IAAI,CAAC,CAAA,CAAY,CAAE,CAC7C,IAAMC,EAAS,IAAI,CAAC,CAAA,CAAS,CAACC,IAAI,GAAGC,IAAI,GAAGC,KAAK,AAC7CH,CAAAA,GAAQ,IAAI,CAAC,CAAA,CAAS,CAACH,MAAM,CAACG,EACnC,CACAL,EAAU,CAAA,EACV,IAAI,CAAC,CAAA,CAAS,CAACG,GAAG,CAACL,EAAWE,EAC/B,CACA,IAAMS,EAAST,CAAO,CAACD,EAAU,EAAIW,EAAeX,UACpD,AAAKU,GACLT,CAAO,CAACD,EAAU,CAAGU,EACdA,GAFa,IAGrB,EAAA,IAAA,CAEAE,YAAAA,CAAe,CACdlB,EACAK,EACAC,EAGAa,KAEA,IAAMC,EAAa,IAAI,CAACrB,aAAa,CAACC,GAEtC,GAAI,CAACoB,GAAc,CAAC,IAAI,CAACxB,MAAM,CAAE,OAAO,KAExC,IAAMyB,EAAeD,EAAWE,OAAO,CAAC,EAAE,EAAEC,MAAQ,KACpD,GAAI,CAAClB,EAAW,OAAOgB,EAEvB,IAAMd,EAAU,IAAI,CAAC,CAAA,CAAW,CAACF,EAAWC,GAC5C,GAAI,CAACC,EAAS,OAAOc,CACjBF,CAAAA,GAAKK,SAASjB,CAAAA,EAAQiB,OAAO,CAAGL,EAAIK,OAAO,AAAPA,EAExC,IAAMC,EAAWlB,CAAO,CAACP,EAAS,CAClC,GAAIyB,EAAU,OAAOA,EAOrB,GAAI,CAL2BC,EAC9B,IAAI,CAAC9B,MAAM,CAAC+B,QAAQ,CAACP,EAAWO,QAAQ,CAAC,CACzCpB,GAG4B,OAAOc,EAEpC,IAAMO,EAAYC,EAAmBT,EAAWE,OAAO,SAEvD,AAAKM,GAELrB,CAAO,CAACP,EAAS,CAAG4B,EACbA,GAHgB,IAIxB,EAAA,IAAA,CAEAE,YAAAA,CAAe,CACd3B,EACAE,EACAC,EAGAa,KAEA,IAAMY,EAAa,IAAI,CAAC7B,aAAa,CAACC,GAEtC,GAAI,CAAC4B,GAAc,CAAC,IAAI,CAACnC,MAAM,CAAE,OAAO,KAExC,GAAI,CAACS,GAAa,CAACC,EAAW,OAAOyB,EAAWV,YAAY,CAE5D,IAAMd,EAAU,IAAI,CAAC,CAAA,CAAW,CAACF,EAAWC,GAC5C,GAAI,CAACC,EAAS,OAAOwB,EAAWV,YAAY,AACxCF,CAAAA,GAAKK,SAASjB,CAAAA,EAAQiB,OAAO,CAAGL,EAAIK,OAAO,AAAPA,EAExC,IAAMC,EAAWlB,CAAO,CAACJ,EAAS,CAClC,GAAIsB,EAAU,OAAOA,EAOrB,GAAI,CAL2BC,EAC9B,IAAI,CAAC9B,MAAM,CAAC+B,QAAQ,CAACI,EAAWJ,QAAQ,CAAC,CACzCpB,GAG4B,OAAOwB,EAAWV,YAAY,CAE3D,GACCU,EAAWC,UAAU,CAAG,KACxBC,AAAgB,IAAhBA,KAAKC,MAAM,GAAWH,EAAWC,UAAU,CAG3C,OADAzB,CAAO,CAACJ,EAAS,CAAG4B,EAAWV,YAAY,CACpCU,EAAWV,YAAY,CAG/B,IAAMc,EAAYN,EAAmBE,EAAWT,OAAO,SAEvD,AAAKa,GAEL5B,CAAO,CAACJ,EAAS,CAAGgC,EACbA,GAHgB,IAIxB,EAxHC,IAAI,CAAC,CAAA,CAAM,CAAG5C,EACd,IAAI,CAAC,CAAA,CAAY,CAAGC,GA7BO,GA8B5B,CAkBA,CAAA,CAAW,AAAA,AAqGZ"}
1
+ {"version":3,"file":"server.mjs","sources":["../src/server.ts","../src/tokenSource.ts"],"sourcesContent":["import { ParsedUserAgent, parseUserAgent } from './utils/parseUserAgent'\nimport { getVisitorMatchesAudience } from './utils/getVisitorMatchesAudience'\nimport { getRandomTestValue } from './utils/getRandomTestValue'\nimport { BaseImproveSDK } from './base'\nimport { type TokenSource, resolveToken } from './tokenSource'\nimport { ImproveConfiguration, ImproveSetupArgs } from './types'\n\nconst DEFAULT_MAX_VISITORS = 10_000\n// One visitor id with endless user agents would otherwise grow without bound.\nconst MAX_USER_AGENTS_PER_VISITOR = 10\n\ntype Visitor = ParsedUserAgent & {\n\t[testSlug: string]: string\n}\n\n// A Map, not an object: the user agent is the request's, and `__proto__`\n// would be Object.prototype.\ntype VisitorData = Map<string, Visitor>\n\nconst assignment = (visitor: Visitor, slug: string) =>\n\tObject.hasOwn(visitor, slug) ? visitor[slug] : undefined\n\ntype ImproveServerSetupArgs =\n\t| (Omit<ImproveSetupArgs, 'config' | 'baseUrl'> & {\n\t\t\tconfig: ImproveConfiguration\n\t\t\tmaxVisitors?: number\n\t })\n\t| (Omit<ImproveSetupArgs, 'config'> & {\n\t\t\t// An API token (`imp_…`), or a function giving one per request,\n\t\t\t// like `vercelOidc()` from `@obelism/improve-sdk/oidc`.\n\t\t\ttoken: TokenSource\n\t\t\tmaxVisitors?: number\n\t })\n\nexport class ImproveServerSDK extends BaseImproveSDK {\n\t#visitors: Map<string, VisitorData> = new Map()\n\t#maxVisitors: number\n\t#token: TokenSource\n\n\t// @ts-ignore It could be there\n\tconstructor({ token, maxVisitors, ...args }: ImproveServerSetupArgs) {\n\t\tsuper(args)\n\t\tthis.#token = token\n\t\tthis.#maxVisitors = maxVisitors ?? DEFAULT_MAX_VISITORS\n\t}\n\n\tfetchConfig = async (config?: RequestInit) => {\n\t\treturn this._fetchConfig({\n\t\t\t...config,\n\t\t\theaders: {\n\t\t\t\t...config?.headers,\n\t\t\t\ttoken: await resolveToken(this.#token),\n\t\t\t},\n\t\t})\n\t}\n\n\tgetFlagConfig = (flagSlug: string) => this.config?.flags?.[flagSlug]\n\n\tgetTestConfig = (testSlug: string) => this.config?.tests?.[testSlug]\n\n\t// The visitor as seen with this user agent, or null when it can't be\n\t// parsed (an empty user agent), which gets the default like a holdout.\n\t#getVisitor = (visitorId: string, userAgent: string): Visitor | null => {\n\t\tlet visitor = this.#visitors.get(visitorId)\n\t\tif (visitor) {\n\t\t\t// Move to end for LRU freshness\n\t\t\tthis.#visitors.delete(visitorId)\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t} else {\n\t\t\t// Evict oldest entries when at capacity\n\t\t\tif (this.#visitors.size >= this.#maxVisitors) {\n\t\t\t\tconst oldest = this.#visitors.keys().next().value\n\t\t\t\tif (oldest) this.#visitors.delete(oldest)\n\t\t\t}\n\t\t\tvisitor = new Map()\n\t\t\tthis.#visitors.set(visitorId, visitor)\n\t\t}\n\t\tconst known = visitor.get(userAgent)\n\t\tif (known) return known\n\t\tconst parsed = parseUserAgent(userAgent)\n\t\tif (!parsed) return null\n\t\tif (visitor.size >= MAX_USER_AGENTS_PER_VISITOR) {\n\t\t\tvisitor.delete(visitor.keys().next().value!)\n\t\t}\n\t\tvisitor.set(userAgent, parsed)\n\t\treturn parsed\n\t}\n\n\tgetFlagValue = (\n\t\tflagSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst flagConfig = this.getFlagConfig(flagSlug)\n\n\t\tif (!flagConfig || !this.config) return null\n\n\t\tconst defaultValue = flagConfig.options[0]?.slug ?? null\n\t\tif (!visitorId) return defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = assignment(visitor, flagSlug)\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[flagConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return defaultValue\n\n\t\tconst flagValue = getRandomTestValue(flagConfig.options)\n\n\t\tif (!flagValue) return null\n\n\t\tvisitor[flagSlug] = flagValue\n\t\treturn flagValue\n\t}\n\n\tgetTestValue = (\n\t\ttestSlug: string,\n\t\tvisitorId: string,\n\t\tuserAgent: string,\n\t\t// Coarse geo from the request (SSR/middleware), so audiences can target by\n\t\t// country. The browser can't determine this, so it's passed in here.\n\t\tgeo?: { country?: string | undefined },\n\t) => {\n\t\tconst testConfig = this.getTestConfig(testSlug)\n\n\t\tif (!testConfig || !this.config) return null\n\n\t\tif (!visitorId || !userAgent) return testConfig.defaultValue\n\n\t\tconst visitor = this.#getVisitor(visitorId, userAgent)\n\t\tif (!visitor) return testConfig.defaultValue\n\t\tif (geo?.country) visitor.country = geo.country\n\n\t\tconst assigned = assignment(visitor, testSlug)\n\t\tif (assigned) return assigned\n\n\t\tconst visitorMatchesAudience = getVisitorMatchesAudience(\n\t\t\tthis.config.audience[testConfig.audience],\n\t\t\tvisitor,\n\t\t)\n\n\t\tif (!visitorMatchesAudience) return testConfig.defaultValue\n\n\t\tif (\n\t\t\ttestConfig.allocation < 100 &&\n\t\t\tMath.random() * 100 > testConfig.allocation\n\t\t) {\n\t\t\tvisitor[testSlug] = testConfig.defaultValue\n\t\t\treturn testConfig.defaultValue\n\t\t}\n\n\t\tconst testValue = getRandomTestValue(testConfig.options)\n\n\t\tif (!testValue) return null\n\n\t\tvisitor[testSlug] = testValue\n\t\treturn testValue\n\t}\n}\n","// A token, or a function that gives one per request: a host OIDC token from\n// ./oidc, or anything else that rotates.\nexport type TokenSource = string | (() => string | Promise<string>)\n\nexport const resolveToken = (token: TokenSource) =>\n\ttypeof token === 'function' ? token() : token\n"],"names":["assignment","visitor","slug","Object","hasOwn","undefined","ImproveServerSDK","BaseImproveSDK","token","maxVisitors","args","Map","fetchConfig","config","_fetchConfig","headers","getFlagConfig","flagSlug","flags","getTestConfig","testSlug","tests","visitorId","userAgent","get","delete","set","size","oldest","keys","next","value","known","parsed","parseUserAgent","getFlagValue","geo","flagConfig","defaultValue","options","country","assigned","getVisitorMatchesAudience","audience","flagValue","getRandomTestValue","getTestValue","testConfig","allocation","Math","random","testValue"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ type TokenSource = string | (() => string | Promise<string>);
2
+
3
+ export type { TokenSource as T };
4
+ //# sourceMappingURL=tokenSource-my2ymemb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenSource-my2ymemb.d.ts","sources":["../src/tokenSource.ts"],"mappings":"AAAA,KAAK,WAAW,4BAA4B,OAAO;;","names":[]}
package/dist/types.d.ts CHANGED
@@ -14,11 +14,12 @@ type AudienceParamKey = keyof typeof AUDIENCE_PARAMS;
14
14
  declare class BaseImproveSDK {
15
15
  #private;
16
16
  organizationId: string;
17
+ spaceId: string | undefined;
17
18
  environment: ImproveEnvironmentOption;
18
19
  state: ImproveTestState | undefined;
19
20
  config: ImproveConfiguration | null;
20
21
  _baseUrl: undefined | string;
21
- constructor({ organizationId, environment, state, config, fetchTimeout, baseUrl, configRetries, configRevalidate, disableWarnings, }: ImproveSetupArgs);
22
+ constructor({ organizationId, spaceId, environment, state, config, fetchTimeout, baseUrl, configRetries, configRevalidate, disableWarnings, }: ImproveSetupArgs);
22
23
  _fetchConfig: (config?: RequestInit) => Promise<ImproveConfiguration | undefined>;
23
24
  loadConfig: (config: ImproveConfiguration) => void;
24
25
  generateVisitorId: () => string;
@@ -37,6 +38,12 @@ type ImproveEnvironmentOption = 'develop' | 'staging' | 'production';
37
38
  type ImproveTestState = 'draft' | 'active' | 'finished' | 'archived';
38
39
  type ImproveSetupArgs = {
39
40
  organizationId: string;
41
+ /**
42
+ * The space (`space_…`): one site or project of the organization, with its
43
+ * own tests, flags, audiences, tokens and allowed origins. Without it, the
44
+ * organization's first space. Copy it from Settings → Implementation.
45
+ */
46
+ spaceId?: string | undefined;
40
47
  environment: ImproveEnvironmentOption;
41
48
  state?: ImproveTestState | undefined;
42
49
  config?: ImproveConfiguration | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sources":["../src/config/audiences.ts","../src/base.ts","../src/types.ts"],"mappings":"AAAA;AACA;AACA;AACA;AACA,cAAc,eAAe;AAC7B,sBAAsB,aAAa;AACnC;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,gBAAgB,eAAe;;ACVpD,cAAc,cAAc;AAC5B;AACA;AACA,iBAAiB,wBAAwB;AACzC,WAAW,gBAAgB;AAC3B,YAAY,oBAAoB;AAChC;AACA,0IAA0I,gBAAgB;AAC1J,4BAA4B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACxE,yBAAyB,oBAAoB;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACpBA,KAAK,wBAAwB;AAC7B,KAAK,gBAAgB;AACrB,KAAK,gBAAgB;AACrB;AACA,iBAAiB,wBAAwB;AACzC,YAAY,gBAAgB;AAC5B,aAAa,oBAAoB;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,2BAA2B;AAChC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,GAAG,2BAA2B;AACnD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,eAAe;AACpB,WAAW,gBAAgB;AAC3B;AACA;AACA,KAAK,aAAa;AAClB,WAAW,eAAe;AAC1B,aAAa,eAAe;AAC5B,gBAAgB,eAAe;AAC/B;AACA;AACA;AACA;AACA;AACA,aAAa,gBAAgB;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,sBAAsB;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B,YAAY,aAAa;AACzB;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA,KAAK,oBAAoB;AACzB,YAAY,gBAAgB;AAC5B;AACA,KAAK,eAAe;AACpB,8BAA8B,oBAAoB;AAClD;AACA,KAAK,oBAAoB;AACzB;AACA;AACA,WAAW,YAAY;AACvB,WAAW,YAAY;AACvB,cAAc,eAAe;AAC7B;;","names":[]}
1
+ {"version":3,"file":"types.d.ts","sources":["../src/config/audiences.ts","../src/base.ts","../src/types.ts"],"mappings":"AAAA;AACA;AACA;AACA;AACA,cAAc,eAAe;AAC7B,sBAAsB,aAAa;AACnC;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,gBAAgB,eAAe;;ACVpD,cAAc,cAAc;AAC5B;AACA;AACA;AACA,iBAAiB,wBAAwB;AACzC,WAAW,gBAAgB;AAC3B,YAAY,oBAAoB;AAChC;AACA,mJAAmJ,gBAAgB;AACnK,4BAA4B,WAAW,KAAK,OAAO,CAAC,oBAAoB;AACxE,yBAAyB,oBAAoB;AAC7C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACrBA,KAAK,wBAAwB;AAC7B,KAAK,gBAAgB;AACrB,KAAK,gBAAgB;AACrB;AACA;AACA;AACA;AACA;AACA;AACA;AACA,iBAAiB,wBAAwB;AACzC,YAAY,gBAAgB;AAC5B,aAAa,oBAAoB;AACjC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,iBAAiB;AACtB;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,2BAA2B;AAChC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,gBAAgB,GAAG,2BAA2B;AACnD;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,eAAe;AACpB,WAAW,gBAAgB;AAC3B;AACA;AACA,KAAK,aAAa;AAClB,WAAW,eAAe;AAC1B,aAAa,eAAe;AAC5B,gBAAgB,eAAe;AAC/B;AACA;AACA;AACA;AACA;AACA,aAAa,gBAAgB;AAC7B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,sBAAsB;AAC3B;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,aAAa,MAAM;AACnB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,aAAa;AAClB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,KAAK,WAAW;AAChB;AACA;AACA;AACA;AACA;AACA,aAAa,iBAAiB;AAC9B,YAAY,aAAa;AACzB;AACA,KAAK,YAAY;AACjB,0BAA0B,WAAW;AACrC;AACA,KAAK,oBAAoB;AACzB,YAAY,gBAAgB;AAC5B;AACA,KAAK,eAAe;AACpB,8BAA8B,oBAAoB;AAClD;AACA,KAAK,oBAAoB;AACzB;AACA;AACA,WAAW,YAAY;AACvB,WAAW,YAAY;AACvB,cAAc,eAAe;AAC7B;;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@obelism/improve-sdk",
3
3
  "description": "Obelism Improve SDK",
4
- "version": "3.1.1",
4
+ "version": "3.3.0",
5
5
  "keywords": [
6
6
  "ab-tests",
7
7
  "feature-flags",
@@ -32,6 +32,11 @@
32
32
  },
33
33
  "./types": {
34
34
  "types": "./dist/types.d.ts"
35
+ },
36
+ "./oidc": {
37
+ "types": "./dist/oidc.d.ts",
38
+ "import": "./dist/oidc.mjs",
39
+ "require": "./dist/oidc.cjs"
35
40
  }
36
41
  },
37
42
  "typesVersions": {
@@ -59,20 +64,27 @@
59
64
  "clean": "rm -rf ./node_modules && rm -rf ./dist && rm -rf ./.turbo"
60
65
  },
61
66
  "dependencies": {
62
- "@obelism/numbers-sdk": "^0.1.0"
67
+ "@obelism/numbers-sdk": "^0.2.0"
63
68
  },
64
69
  "peerDependencies": {
65
- "ua-parser-js": "^2.0.0"
70
+ "ua-parser-js": "^2.0.0",
71
+ "@vercel/oidc": "^4.0.0"
66
72
  },
67
73
  "devDependencies": {
68
74
  "@obelism/sdk-typescript-config": "^0.0.0",
69
75
  "@typescript/typescript6": "^6.0.2",
76
+ "@vercel/oidc": "4.0.3",
70
77
  "bunchee": "^7.0.2",
71
- "jsdom": "^30.1.2",
72
- "npm-run-all2": "^9.0.2",
73
- "oxlint": "^1.88.0",
78
+ "jsdom": "^30.1.3",
79
+ "npm-run-all2": "^9.0.4",
80
+ "oxlint": "^1.89.0",
74
81
  "typescript": "^7.0.2",
75
82
  "ua-parser-js": "^2.0.10",
76
- "vitest": "^5.0.4"
83
+ "vitest": "^5.0.5"
84
+ },
85
+ "peerDependenciesMeta": {
86
+ "@vercel/oidc": {
87
+ "optional": true
88
+ }
77
89
  }
78
90
  }
@@ -1,2 +0,0 @@
1
- var e=require("ua-parser-js");let t=["wearable","mobile","tablet","console","smarttv","embedded","desktop"],r=["chrome","safari","firefox","edge","ie","samsung internet","social","other"],i=["mac os","ios","android","windows","unix"],n=["tiktok","wechat","weibo","snapchat","klarna","Line","instagram","facebook","alipay","Baidu"],s=["wearable","mobile","tablet"],o="visi",a=e=>new Promise(t=>setTimeout(t,e)),u={timeout:"Configuration request timed out",network:"Configuration request could not reach the server",unauthorized:"Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment","rate-limited":"Configuration request was rate limited (429): too many requests, or the organization usage limit was reached",server:"Configuration request failed with a server error",client:"Configuration request was rejected by the server","invalid-response":"Configuration response could not be parsed as JSON"};class l extends Error{constructor(e,t){super(u[e]),this.name="ImproveFetchError",this.reason=e,this.status=t?.status,this.retryAfterMs=t?.retryAfterMs,t&&"cause"in t&&(this.cause=t.cause)}get isRetryable(){return"rate-limited"===this.reason||"server"===this.reason||"timeout"===this.reason||"network"===this.reason}}let h=e=>429===e?"rate-limited":401===e||403===e?"unauthorized":e>=500?"server":"client",c="ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",f=e=>{if("object"!=typeof e||null===e||Array.isArray(e))throw new l("invalid-response");return{name:"string"==typeof e.name?e.name:"",version:"number"==typeof e.version?e.version:0,tests:e.tests??{},flags:e.flags??{},audience:e.audience??{}}},d=(e,t=Date.now())=>{if(!e)return;let r=Number(e);if(Number.isFinite(r))return Math.max(0,Math.round(1e3*r));let i=Date.parse(e);if(!Number.isNaN(i))return Math.max(0,i-t)},g=(e,t,r)=>Math.round(Math.random()*Math.min(r,t*2**e)),m=async(e=1e3,t)=>(await a(e),t?.abort(),null),w=(e=3e3,t,r)=>{let i=new AbortController;return Promise.race([fetch(t,{...r,signal:i.signal}),m(e,i)])};exports.ANALYTICS_PATH="/api/log",exports.BaseImproveSDK=class{#e;#t;constructor({organizationId:e,environment:t,state:r,config:i,fetchTimeout:n,baseUrl:s,configRetries:u,configRevalidate:m,disableWarnings:p}){this.environment="develop",this.#e=null,this.#t=!1,this.config=null,this._fetchConfig=async e=>{if(this.config)return;if(!this.#e)throw Error("No config fetch setup provided");let{url:t,timeout:r,retries:i,revalidate:n}=this.#e,s={...e};"number"==typeof n&&(s.next={...e?.next,revalidate:n});let o=new l("network");for(let e=0;e<=i;e++){let i;e>0&&await a(o.retryAfterMs??g(e-1,300,3e3));try{i=await w(r,t,s)}catch(e){o=new l(e instanceof Error&&"AbortError"===e.name?"timeout":"network",{cause:e});continue}if(!i){o=new l("timeout");continue}if(i.ok){try{this.config=f(await i.json())}catch(e){o=e instanceof l?e:new l("invalid-response",{cause:e});continue}return this.config}if(!(o=new l(h(i.status),{status:i.status,retryAfterMs:d(i.headers.get("Retry-After"))})).isRetryable)break}throw o},this.loadConfig=e=>{this.config=e},this.generateVisitorId=()=>[o,(function(e=5){return e&&"number"==typeof e?Array(e).fill("").reduce(e=>e+=c.charAt(Math.floor(Math.random()*c.length)),""):""})(26).toUpperCase()].join("_"),this.getVisitorCookieName=()=>"visitorId",this._warn=e=>{this.#t||"u">typeof console&&console.warn(`[Improve] ${e}`)},this.validateTestValue=(e,t)=>{if(!this.config)throw Error("Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup");let r=this.config.tests[e];if(!r)throw Error(`No config found for ${e}`);return!!r.options.find(e=>e.slug===t)},this.validateVisitorId=e=>{let t=e.split("_");if(2!==t.length)return!1;let[r,i]=t;return r===o&&26===i.length},this.organizationId=e,this.environment=t,this.state=r,this._baseUrl=s||"https://improve.obelism.studio",this.#t=p??!1,i?this.config=i:this.#e={url:[`${this._baseUrl}/config`,this.organizationId,this.environment,this.state||"active"].join("/"),timeout:n||3e3,retries:u??2,revalidate:m}}},exports.getRandomTestValue=e=>{let[t]=e;if(!t)return null;if(1===e.length)return t.slug;let r=Math.random()*e.reduce((e,{split:t})=>e+t,0);return(e.find(({split:e})=>(r-=e)<=0)||t).slug},exports.getVisitorMatchesAudience=(e,t)=>!e||Object.entries(e).every(([e,r])=>t[e]===r),exports.parseUserAgent=o=>{var a;if(!o||"string"!=typeof o)return null;let u=new e.UAParser(o).getResult(),l=(a=u.device.type)&&t.includes(a)?a:"desktop";return{pointer:s.includes(l)?"coarse":"fine",device:l,browser:((e="")=>{if(!e)return"other";let t=e.toLowerCase(),i=r.find(e=>t.includes(e));return i||(n.includes(t)?"social":"other")})(u.browser.name),os:((e="")=>{if(!e)return"unix";let t=e.toLowerCase(),r=i.find(e=>t.includes(e));return r||"unix"})(u.os.name)}};
2
- //# sourceMappingURL=base-gi7y83ve.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"base-gi7y83ve.js","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/config/constants.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/getTimeoutError.ts","../src/utils/timeoutFetch.ts","../src/config/urls.ts","../src/base.ts","../src/utils/getRandomTestValue.ts","../src/utils/getVisitorMatchesAudience.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Location — coarse IP-geolocation (ISO 3166-1 alpha-2 country code). Values\n\t//? aren't enumerated here (any country code is valid); the key exists so\n\t//? audiences can target `country`. Resolved server-side (SSR/middleware) from\n\t//? the request, since the browser can't determine it from the user agent.\n\tcountry: [] as ReadonlyArray<string>,\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n\t// Coarse geo, not derived from the user agent — merged in server-side\n\t// (SSR/middleware) so audiences can target by country. Absent client-side.\n\tcountry?: string\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number | undefined\n\tretryAfterMs?: number | undefined\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: {\n\t\t\tstatus?: number | undefined\n\t\t\tretryAfterMs?: number | undefined\n\t\t\tcause?: unknown\n\t\t},\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\n// Events go to Obelism numbers (the numbers SDK's default endpoint); only a\n// custom `baseUrl` sends them to its own /api/log.\nexport const ANALYTICS_PATH = `/api/log`\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number | undefined\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState | undefined\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tconst [first] = options\n\tif (!first) return null\n\tif (options.length === 1) return first.slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || first\n\n\treturn match.slug\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","VISITOR_ID_PREFIX","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","options","name","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","audience","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","Math","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","random","min","getTimeoutError","controller","abort","timeoutFetch","url","AbortController","race","fetch","signal","organizationId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","reduce","acc","charAt","floor","length","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","find","option","slug","validateVisitorId","possibleVisitorId","visitorIdParts","split","key","value","_baseUrl","first","match","visitorParams","Object","entries","every","paramKey","paramValue","userAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","toLowerCase","browserTypeMatch","browserType","os","formatOs","osName","osTypeMatch","osType"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"base-i6scrpy2.mjs","sources":["../src/config/audiences.ts","../src/utils/parseUserAgent.ts","../src/utils/getVisitorMatchesAudience.ts","../src/utils/getRandomTestValue.ts","../src/config/constants.ts","../src/config/urls.ts","../src/utils/delay.ts","../src/utils/errors.ts","../src/utils/getRandomString.ts","../src/utils/normalizeConfig.ts","../src/utils/retry.ts","../src/utils/getTimeoutError.ts","../src/utils/timeoutFetch.ts","../src/base.ts"],"sourcesContent":["/**\n * @constant AUDIENCE_PARAMS\n * @description All posibile audience tracking options\n */\nexport const AUDIENCE_PARAMS = {\n\t//? Location — coarse IP-geolocation (ISO 3166-1 alpha-2 country code). Values\n\t//? aren't enumerated here (any country code is valid); the key exists so\n\t//? audiences can target `country`. Resolved server-side (SSR/middleware) from\n\t//? the request, since the browser can't determine it from the user agent.\n\tcountry: [] as ReadonlyArray<string>,\n\t//? Technical\n\tpointer: ['coarse', 'fine'],\n\tdevice: [\n\t\t'wearable',\n\t\t'mobile',\n\t\t'tablet',\n\t\t'console',\n\t\t'smarttv',\n\t\t'embedded',\n\t\t'desktop',\n\t],\n\tbrowser: [\n\t\t'chrome',\n\t\t'safari',\n\t\t'firefox',\n\t\t'edge',\n\t\t'ie',\n\t\t'samsung internet',\n\t\t'social',\n\t\t'other',\n\t],\n\tos: ['mac os', 'ios', 'android', 'windows', 'unix'],\n} as const\n\n//? Generated types\nexport type AudienceParamKey = keyof typeof AUDIENCE_PARAMS\n\nexport type AudienceParamPointer = (typeof AUDIENCE_PARAMS.pointer)[number]\n\nexport type AudienceParamDevice = (typeof AUDIENCE_PARAMS.device)[number]\n\nexport type AudienceParamBrowser = (typeof AUDIENCE_PARAMS.browser)[number]\n\nexport type AudienceParamOs = (typeof AUDIENCE_PARAMS.os)[number]\n\nexport const AUDIENCE_PARAM_KEYS = Object.keys(\n\tAUDIENCE_PARAMS,\n) as ReadonlyArray<AudienceParamKey>\n","import { UAParser } from 'ua-parser-js'\n\nimport {\n\tAUDIENCE_PARAMS,\n\tAudienceParamBrowser,\n\tAudienceParamDevice,\n\tAudienceParamOs,\n\tAudienceParamPointer,\n} from '../config/audiences'\n\nexport type ParsedUserAgent = {\n\tpointer: AudienceParamPointer\n\tdevice: AudienceParamDevice\n\tbrowser: AudienceParamBrowser\n\tos: AudienceParamOs\n\t// Coarse geo, not derived from the user agent — merged in server-side\n\t// (SSR/middleware) so audiences can target by country. Absent client-side.\n\tcountry?: string\n}\n\nconst formatDeviceType = (deviceType?: string): AudienceParamDevice => {\n\tif (\n\t\tdeviceType &&\n\t\tAUDIENCE_PARAMS.device.includes(deviceType as AudienceParamDevice)\n\t) {\n\t\treturn deviceType as AudienceParamDevice\n\t}\n\treturn 'desktop'\n}\n\nconst SOCIAL_BROWSERS: ReadonlyArray<string> = [\n\t'tiktok',\n\t'wechat',\n\t'weibo',\n\t'snapchat',\n\t'klarna',\n\t'Line',\n\t'instagram',\n\t'facebook',\n\t'alipay',\n\t'Baidu',\n]\n\nconst formatBrowser = (browserName: string = ''): AudienceParamBrowser => {\n\tif (!browserName) return 'other'\n\n\tconst name = browserName.toLowerCase()\n\n\tconst browserTypeMatch = AUDIENCE_PARAMS.browser.find((browserType) => {\n\t\treturn name.includes(browserType)\n\t})\n\n\tif (browserTypeMatch) return browserTypeMatch\n\n\tif (SOCIAL_BROWSERS.includes(name)) return 'social'\n\treturn 'other'\n}\n\nconst PRIMARY_TOUCH_DEVICES: ReadonlyArray<AudienceParamDevice> = [\n\t'wearable',\n\t'mobile',\n\t'tablet',\n]\n\nconst formatPointer = (device: AudienceParamDevice) => {\n\treturn PRIMARY_TOUCH_DEVICES.includes(device) ? 'coarse' : 'fine'\n}\n\nconst formatOs = (osName: string = ''): AudienceParamOs => {\n\tif (!osName) return 'unix'\n\n\tconst os = osName.toLowerCase()\n\n\tconst osTypeMatch = AUDIENCE_PARAMS.os.find((osType) => {\n\t\treturn os.includes(osType)\n\t})\n\n\tif (osTypeMatch) return osTypeMatch\n\treturn 'unix'\n}\n\nexport const parseUserAgent = (userAgent: string): ParsedUserAgent | null => {\n\tif (!userAgent || typeof userAgent !== 'string') return null\n\n\tconst parser = new UAParser(userAgent)\n\tconst results = parser.getResult()\n\n\tconst device = formatDeviceType(results.device.type)\n\n\treturn {\n\t\tpointer: formatPointer(device),\n\t\tdevice: device,\n\t\tbrowser: formatBrowser(results.browser.name),\n\t\tos: formatOs(results.os.name),\n\t}\n}\n","import { ImproveAudienceValue } from '../types'\nimport { AudienceParamKey } from '../config/audiences'\nimport { ParsedUserAgent } from './parseUserAgent'\n\nexport const getVisitorMatchesAudience = (\n\taudience: ImproveAudienceValue | undefined,\n\tvisitorParams: ParsedUserAgent,\n) => {\n\tif (!audience) return true\n\treturn Object.entries(audience).every(([paramKey, paramValue]) => {\n\t\treturn visitorParams[paramKey as AudienceParamKey] === paramValue\n\t})\n}\n","import { ImproveTestOption } from '../types'\n\n// function cryptoRandom() {\n// \treturn crypto.getRandomValues(new Uint32Array(1))[0] / (0xffffffff + 1)\n// }\n\nexport const getRandomTestValue = (options: ImproveTestOption[]) => {\n\tconst [first] = options\n\tif (!first) return null\n\tif (options.length === 1) return first.slug\n\n\t// Get a random number between 0 and 1\n\tconst sum = options.reduce((acc, { split }) => acc + split, 0)\n\tlet value = Math.random() * sum\n\n\tconst match =\n\t\toptions.find(({ split }) => {\n\t\t\tvalue -= split\n\t\t\treturn value <= 0\n\t\t}) || first\n\n\treturn match.slug\n}\n","export const COOKIE_NAME_VISITOR = 'visitorId'\n\nexport const VISITOR_ID_PREFIX = 'visi'\n\nexport const VISITOR_ID_LENGTH = 26\n\nexport const VISITOR_ID_SEPARATOR = '_'\n\n/** Default number of retries for a failed config fetch (network / 5xx / 429). */\nexport const CONFIG_RETRY_COUNT = 2\n\n/** Base delay for config-fetch backoff, in ms. */\nexport const CONFIG_RETRY_BASE_DELAY_MS = 300\n\n/** Upper bound for a single config-fetch backoff wait, in ms. */\nexport const CONFIG_RETRY_MAX_DELAY_MS = 3000\n","export const BASE_URL = 'https://improve.obelism.studio'\nexport const CONFIG_PATH = `/config`\n// Events go to Obelism numbers (the numbers SDK's default endpoint); only a\n// custom `baseUrl` sends them to its own /api/log.\nexport const ANALYTICS_PATH = `/api/log`\n","export const delay = (ms: number) =>\n\tnew Promise((resolve) => setTimeout(resolve, ms))\n","export type ImproveFetchErrorReason =\n\t| 'timeout'\n\t| 'network'\n\t| 'unauthorized'\n\t| 'rate-limited'\n\t| 'server'\n\t| 'client'\n\t| 'invalid-response'\n\nconst REASON_MESSAGES: Record<ImproveFetchErrorReason, string> = {\n\ttimeout: 'Configuration request timed out',\n\tnetwork: 'Configuration request could not reach the server',\n\tunauthorized:\n\t\t'Configuration request was rejected (403): verify the organizationId, the server token, and that the request origin is whitelisted for this environment',\n\t'rate-limited':\n\t\t'Configuration request was rate limited (429): too many requests, or the organization usage limit was reached',\n\tserver: 'Configuration request failed with a server error',\n\tclient: 'Configuration request was rejected by the server',\n\t'invalid-response': 'Configuration response could not be parsed as JSON',\n}\n\n/**\n * Error thrown by the config fetch layer. Carries the HTTP `status`, a coarse\n * `reason`, and (when the server sent one) the `Retry-After` delay in ms, so\n * callers can distinguish a misconfiguration (unauthorized) from a transient\n * failure (rate-limited/server/timeout) instead of guessing from a string.\n */\nexport class ImproveFetchError extends Error {\n\treason: ImproveFetchErrorReason\n\tstatus?: number | undefined\n\tretryAfterMs?: number | undefined\n\n\tconstructor(\n\t\treason: ImproveFetchErrorReason,\n\t\toptions?: {\n\t\t\tstatus?: number | undefined\n\t\t\tretryAfterMs?: number | undefined\n\t\t\tcause?: unknown\n\t\t},\n\t) {\n\t\tsuper(REASON_MESSAGES[reason])\n\t\tthis.name = 'ImproveFetchError'\n\t\tthis.reason = reason\n\t\tthis.status = options?.status\n\t\tthis.retryAfterMs = options?.retryAfterMs\n\t\tif (options && 'cause' in options) {\n\t\t\t;(this as { cause?: unknown }).cause = options.cause\n\t\t}\n\t}\n\n\t/** Whether retrying the request could plausibly succeed. */\n\tget isRetryable() {\n\t\treturn (\n\t\t\tthis.reason === 'rate-limited' ||\n\t\t\tthis.reason === 'server' ||\n\t\t\tthis.reason === 'timeout' ||\n\t\t\tthis.reason === 'network'\n\t\t)\n\t}\n}\n\n/** Map an HTTP status onto a coarse {@link ImproveFetchErrorReason}. */\nexport const getReasonFromStatus = (\n\tstatus: number,\n): ImproveFetchErrorReason => {\n\tif (status === 429) return 'rate-limited'\n\tif (status === 401 || status === 403) return 'unauthorized'\n\tif (status >= 500) return 'server'\n\treturn 'client'\n}\n","/**\n * @constant CHAR_SET\n * @description Key/value set of characters to be used for generation strings\n */\nconst CHAR_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'\n\n/**\n * @function getRandomString\n * @description Generate a random string\n */\nexport function getRandomString(characters: number = 5): string {\n\tif (!characters || typeof characters !== 'number') return ''\n\treturn Array(characters)\n\t\t.fill('')\n\t\t.reduce((acc) => {\n\t\t\tacc += CHAR_SET.charAt(Math.floor(Math.random() * CHAR_SET.length))\n\t\t\treturn acc\n\t\t}, '')\n}\n","import { ImproveConfiguration } from '../types'\nimport { ImproveFetchError } from './errors'\n\n/**\n * @function normalizeConfig\n * @description Coerce a parsed config response into a well-formed\n * {@link ImproveConfiguration}. The backend returns `200 {}` for an unknown\n * org/environment (and on its own internal error). An empty object is truthy,\n * so it slips past `if (!this.config)` guards and then crashes downstream on\n * `config.tests[slug]` (`Cannot read properties of undefined`). Guaranteeing\n * `tests`/`flags`/`audience` are always objects lets the SDK fail open to\n * default values instead of throwing during render/middleware.\n *\n * Throws {@link ImproveFetchError} `invalid-response` when the body isn't an\n * object at all (e.g. an array, string, or null).\n */\nexport const normalizeConfig = (body: unknown): ImproveConfiguration => {\n\tif (typeof body !== 'object' || body === null || Array.isArray(body)) {\n\t\tthrow new ImproveFetchError('invalid-response')\n\t}\n\n\tconst raw = body as Partial<ImproveConfiguration>\n\n\treturn {\n\t\tname: typeof raw.name === 'string' ? raw.name : '',\n\t\tversion: typeof raw.version === 'number' ? raw.version : 0,\n\t\ttests: raw.tests ?? {},\n\t\tflags: raw.flags ?? {},\n\t\taudience: raw.audience ?? {},\n\t}\n}\n","/**\n * @function parseRetryAfterMs\n * @description Parse an HTTP `Retry-After` header into milliseconds. Supports\n * both the delta-seconds form (`\"120\"`) and the HTTP-date form\n * (`\"Wed, 21 Oct 2026 07:28:00 GMT\"`). Returns `undefined` when the header is\n * absent or unparseable so the caller can fall back to computed backoff.\n *\n * @param {string | null} [header] - the raw `Retry-After` header value\n * @param {number} [now] - current epoch ms (injectable for testing)\n */\nexport const parseRetryAfterMs = (\n\theader: string | null | undefined,\n\tnow: number = Date.now(),\n): number | undefined => {\n\tif (!header) return undefined\n\n\tconst seconds = Number(header)\n\tif (Number.isFinite(seconds)) return Math.max(0, Math.round(seconds * 1000))\n\n\tconst date = Date.parse(header)\n\tif (Number.isNaN(date)) return undefined\n\n\treturn Math.max(0, date - now)\n}\n\n/**\n * @function getBackoffDelayMs\n * @description Exponential backoff with full jitter, capped at `maxDelay`.\n * Jitter spreads retries so concurrent clients don't stampede the origin in\n * lockstep after a shared failure.\n *\n * @param {number} attempt - zero-based retry attempt index\n * @param {number} baseDelay - base delay in ms\n * @param {number} maxDelay - upper bound in ms\n */\nexport const getBackoffDelayMs = (\n\tattempt: number,\n\tbaseDelay: number,\n\tmaxDelay: number,\n): number => {\n\tconst exponential = Math.min(maxDelay, baseDelay * 2 ** attempt)\n\treturn Math.round(Math.random() * exponential)\n}\n","import { delay } from './delay'\n\n/**\n * @async @function getTimeoutError\n * @description Throw an error after a delay\n *\n * @param {number} [timeout] - time in ms after to reject default: 1000\n * @param {AbortController} [controller] - (optional) AbortController to abort the request\n */\nexport const getTimeoutError = async (\n\ttimeout: number = 1000,\n\tcontroller: AbortController,\n): Promise<null> => {\n\tawait delay(timeout)\n\tcontroller?.abort()\n\treturn null\n}\n","import { getTimeoutError } from './getTimeoutError'\n\n/**\n * @async @function timeoutFetch\n * @description Fetch with a timeout\n *\n * @param {number} timeout - time in ms after to reject default: 3000\n * @param {string} url - url to fetch\n * @param {Object} [options] - (optional) options to pass to fetch\n */\nexport const timeoutFetch = (\n\ttimeout: number = 3000,\n\turl: string,\n\toptions?: object,\n) => {\n\tconst controller = new AbortController()\n\treturn Promise.race([\n\t\tfetch(url, {\n\t\t\t...options,\n\t\t\tsignal: controller.signal,\n\t\t}),\n\t\tgetTimeoutError(timeout, controller),\n\t])\n}\n","import {\n\tCONFIG_RETRY_BASE_DELAY_MS,\n\tCONFIG_RETRY_COUNT,\n\tCONFIG_RETRY_MAX_DELAY_MS,\n\tCOOKIE_NAME_VISITOR,\n\tVISITOR_ID_LENGTH,\n\tVISITOR_ID_PREFIX,\n\tVISITOR_ID_SEPARATOR,\n} from './config/constants'\nimport { CONFIG_PATH, BASE_URL } from './config/urls'\nimport {\n\tImproveConfiguration,\n\tImproveEnvironmentOption,\n\tImproveSetupArgs,\n\tImproveTestState,\n} from './types'\nimport { delay } from './utils/delay'\nimport { getReasonFromStatus, ImproveFetchError } from './utils/errors'\nimport { getRandomString } from './utils/getRandomString'\nimport { normalizeConfig } from './utils/normalizeConfig'\nimport { getBackoffDelayMs, parseRetryAfterMs } from './utils/retry'\nimport { timeoutFetch } from './utils/timeoutFetch'\n\ntype ConfigFetch = {\n\turl: string\n\ttimeout: number\n\tretries: number\n\trevalidate?: number | undefined\n}\n\n// Next.js augments RequestInit with a `next` field for its fetch cache.\ntype RequestInitWithNext = RequestInit & {\n\tnext?: { revalidate?: number; tags?: string[] }\n}\n\nexport class BaseImproveSDK {\n\torganizationId: string\n\tenvironment: ImproveEnvironmentOption = 'develop'\n\tstate: ImproveTestState | undefined\n\n\t#configFetch: ConfigFetch | null = null\n\n\t#warningsDisabled: boolean = false\n\n\tconfig: ImproveConfiguration | null = null\n\n\t_baseUrl: undefined | string\n\n\tconstructor({\n\t\torganizationId,\n\t\tenvironment,\n\t\tstate,\n\t\tconfig,\n\t\tfetchTimeout,\n\t\tbaseUrl,\n\t\tconfigRetries,\n\t\tconfigRevalidate,\n\t\tdisableWarnings,\n\t}: ImproveSetupArgs) {\n\t\tthis.organizationId = organizationId\n\t\tthis.environment = environment\n\t\tthis.state = state\n\t\tthis._baseUrl = baseUrl || BASE_URL\n\t\tthis.#warningsDisabled = disableWarnings ?? false\n\n\t\tif (config) {\n\t\t\tthis.config = config\n\t\t} else {\n\t\t\tthis.#configFetch = {\n\t\t\t\turl: [\n\t\t\t\t\t`${this._baseUrl}${CONFIG_PATH}`,\n\t\t\t\t\tthis.organizationId,\n\t\t\t\t\tthis.environment,\n\t\t\t\t\tthis.state || 'active',\n\t\t\t\t].join('/'),\n\t\t\t\ttimeout: fetchTimeout || 3000,\n\t\t\t\tretries: configRetries ?? CONFIG_RETRY_COUNT,\n\t\t\t\trevalidate: configRevalidate,\n\t\t\t}\n\t\t}\n\t}\n\n\t_fetchConfig = async (config?: RequestInit) => {\n\t\tif (this.config) return\n\n\t\tif (!this.#configFetch) throw new Error('No config fetch setup provided')\n\n\t\tconst { url, timeout, retries, revalidate } = this.#configFetch\n\n\t\t// Opt into the Next.js fetch cache when a revalidate window is configured,\n\t\t// so the datafile is reused across requests instead of re-fetched.\n\t\tconst requestInit: RequestInitWithNext = { ...config }\n\t\tif (typeof revalidate === 'number') {\n\t\t\trequestInit.next = {\n\t\t\t\t...(config as RequestInitWithNext | undefined)?.next,\n\t\t\t\trevalidate,\n\t\t\t}\n\t\t}\n\n\t\t// Seed with a generic network error so an exhausted loop always has a\n\t\t// meaningful, typed error to throw.\n\t\tlet lastError = new ImproveFetchError('network')\n\n\t\tfor (let attempt = 0; attempt <= retries; attempt++) {\n\t\t\tif (attempt > 0) {\n\t\t\t\t// Prefer a server-provided Retry-After; otherwise back off with jitter.\n\t\t\t\tawait delay(\n\t\t\t\t\tlastError.retryAfterMs ??\n\t\t\t\t\t\tgetBackoffDelayMs(\n\t\t\t\t\t\t\tattempt - 1,\n\t\t\t\t\t\t\tCONFIG_RETRY_BASE_DELAY_MS,\n\t\t\t\t\t\t\tCONFIG_RETRY_MAX_DELAY_MS,\n\t\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet res: Response | null\n\t\t\ttry {\n\t\t\t\tres = await timeoutFetch(timeout, url, requestInit)\n\t\t\t} catch (cause) {\n\t\t\t\t// timeoutFetch aborts on timeout (AbortError) and rejects on\n\t\t\t\t// network-level failures — both are transient and retryable.\n\t\t\t\tconst aborted = cause instanceof Error && cause.name === 'AbortError'\n\t\t\t\tlastError = new ImproveFetchError(aborted ? 'timeout' : 'network', {\n\t\t\t\t\tcause,\n\t\t\t\t})\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// timeoutFetch resolves null when the timeout wins the race.\n\t\t\tif (!res) {\n\t\t\t\tlastError = new ImproveFetchError('timeout')\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (res.ok) {\n\t\t\t\ttry {\n\t\t\t\t\tthis.config = normalizeConfig(await res.json())\n\t\t\t\t} catch (cause) {\n\t\t\t\t\tlastError =\n\t\t\t\t\t\tcause instanceof ImproveFetchError\n\t\t\t\t\t\t\t? cause\n\t\t\t\t\t\t\t: new ImproveFetchError('invalid-response', { cause })\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn this.config\n\t\t\t}\n\n\t\t\tlastError = new ImproveFetchError(getReasonFromStatus(res.status), {\n\t\t\t\tstatus: res.status,\n\t\t\t\tretryAfterMs: parseRetryAfterMs(res.headers.get('Retry-After')),\n\t\t\t})\n\n\t\t\t// Auth/validation rejections won't change on retry — fail fast.\n\t\t\tif (!lastError.isRetryable) break\n\t\t}\n\n\t\tthrow lastError\n\t}\n\n\tloadConfig = (config: ImproveConfiguration) => {\n\t\tthis.config = config\n\t}\n\n\tgenerateVisitorId = () => {\n\t\treturn [\n\t\t\tVISITOR_ID_PREFIX,\n\t\t\tgetRandomString(VISITOR_ID_LENGTH).toUpperCase(),\n\t\t].join(VISITOR_ID_SEPARATOR)\n\t}\n\n\tgetVisitorCookieName = () => COOKIE_NAME_VISITOR\n\n\t/**\n\t * Emit a development warning, unless warnings were disabled via the\n\t * `disableWarnings` setup option. Centralized so every SDK warning honors the\n\t * same switch. No-op where `console` is unavailable.\n\t */\n\tprotected _warn = (message: string) => {\n\t\tif (this.#warningsDisabled) return\n\t\tif (typeof console === 'undefined') return\n\t\tconsole.warn(`[Improve] ${message}`)\n\t}\n\n\tvalidateTestValue = (testName: string, testValue: string) => {\n\t\tif (!this.config)\n\t\t\tthrow new Error(\n\t\t\t\t'Config is required before validating, either use `.fetchConfig()`, .loadConfig(config) or provide it during setup',\n\t\t\t)\n\n\t\tconst testConfig = this.config.tests[testName]\n\n\t\tif (!testConfig) throw new Error(`No config found for ${testName}`)\n\n\t\treturn Boolean(\n\t\t\ttestConfig.options.find((option) => option.slug === testValue),\n\t\t)\n\t}\n\n\tvalidateVisitorId = (possibleVisitorId: string) => {\n\t\tconst visitorIdParts = possibleVisitorId.split(VISITOR_ID_SEPARATOR)\n\t\tif (visitorIdParts.length !== 2) return false\n\t\tconst [key, value] = visitorIdParts as [string, string]\n\t\treturn key === VISITOR_ID_PREFIX && value.length === VISITOR_ID_LENGTH\n\t}\n}\n"],"names":["SOCIAL_BROWSERS","PRIMARY_TOUCH_DEVICES","parseUserAgent","userAgent","deviceType","results","parser","UAParser","getResult","device","type","AUDIENCE_PARAMS","includes","pointer","browser","formatBrowser","browserName","name","toLowerCase","browserTypeMatch","find","browserType","os","formatOs","osName","osTypeMatch","osType","getVisitorMatchesAudience","audience","visitorParams","Object","entries","every","paramKey","paramValue","getRandomTestValue","options","first","length","slug","value","Math","random","reduce","acc","split","match","VISITOR_ID_PREFIX","ANALYTICS_PATH","delay","ms","Promise","resolve","setTimeout","REASON_MESSAGES","timeout","network","unauthorized","server","client","ImproveFetchError","Error","reason","status","retryAfterMs","cause","isRetryable","getReasonFromStatus","CHAR_SET","normalizeConfig","body","Array","isArray","raw","version","tests","flags","parseRetryAfterMs","header","now","Date","seconds","Number","isFinite","max","round","date","parse","isNaN","getBackoffDelayMs","attempt","baseDelay","maxDelay","min","getTimeoutError","controller","abort","timeoutFetch","url","AbortController","race","fetch","signal","BaseImproveSDK","organizationId","environment","state","config","fetchTimeout","baseUrl","configRetries","configRevalidate","disableWarnings","_fetchConfig","retries","revalidate","requestInit","next","lastError","res","aborted","ok","json","headers","get","loadConfig","generateVisitorId","getRandomString","characters","fill","charAt","floor","toUpperCase","join","getVisitorCookieName","_warn","message","console","warn","validateTestValue","testName","testValue","testConfig","Boolean","option","validateVisitorId","possibleVisitorId","visitorIdParts","key","_baseUrl"],"mappings":"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"}