diff --git a/apps/angular-example/CHANGELOG.md b/apps/angular-example/CHANGELOG.md index 23531ff2..a0dc3703 100644 --- a/apps/angular-example/CHANGELOG.md +++ b/apps/angular-example/CHANGELOG.md @@ -1,5 +1,12 @@ # angular-example +## 0.0.18 + +### Patch Changes + +- @tryabby/angular@2.0.8 +- @tryabby/devtools@5.0.1 + ## 0.0.17 ### Patch Changes diff --git a/apps/angular-example/package.json b/apps/angular-example/package.json index ead3dee9..9c066f6f 100644 --- a/apps/angular-example/package.json +++ b/apps/angular-example/package.json @@ -1,6 +1,6 @@ { "name": "angular-example", - "version": "0.0.17", + "version": "0.0.18", "private": true, "scripts": { "ng": "ng", diff --git a/apps/web/CHANGELOG.md b/apps/web/CHANGELOG.md index 280fbe50..eceadf59 100644 --- a/apps/web/CHANGELOG.md +++ b/apps/web/CHANGELOG.md @@ -1,5 +1,14 @@ # web +## 0.2.36 + +### Patch Changes + +- Updated dependencies [31d14b7] + - @tryabby/core@5.3.0 + - @tryabby/next@5.1.1 + - @tryabby/devtools@5.0.1 + ## 0.2.35 ### Patch Changes diff --git a/apps/web/abby.config.ts b/apps/web/abby.config.ts index 2fcd4947..d0cf68ec 100644 --- a/apps/web/abby.config.ts +++ b/apps/web/abby.config.ts @@ -23,5 +23,6 @@ export default defineConfig( remoteConfig: { abc: "JSON", }, + cookies: { disableByDefault: true, expiresInDays: 30 }, } ); diff --git a/apps/web/package.json b/apps/web/package.json index cc27ac05..1ec18f37 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "0.2.35", + "version": "0.2.36", "private": true, "scripts": { "build": "next build", diff --git a/apps/web/src/components/Pricing.tsx b/apps/web/src/components/Pricing.tsx index 0f36d976..66523ce0 100644 --- a/apps/web/src/components/Pricing.tsx +++ b/apps/web/src/components/Pricing.tsx @@ -84,6 +84,7 @@ function PricingElement({ string | null; - set: (key: string, value: string) => void; + set: (key: string, value: string, options?: StorageServiceOptions) => void; } export type AbbyConfig< @@ -95,6 +96,10 @@ export type AbbyConfig< settings?: Settings, F.NoInfer, F.NoInfer>; debug?: boolean; fetch?: (typeof globalThis)["fetch"]; + cookies?: { + disableByDefault?: boolean; + expiresInDays?: number; + }; __experimentalCdnUrl?: string; }; @@ -125,6 +130,8 @@ export class Abby< private testOverrides: Map = new Map(); private remoteConfigOverrides = new Map(); + private COOKIE_CONSENT_KEY = "$_abcc_$"; + constructor( private config: F.Narrow< AbbyConfig @@ -153,6 +160,17 @@ export class Abby< }, {} as Record ); + + if (persistantTestStorage) { + this.persistantTestStorage = { + get: (...args) => persistantTestStorage.get(...args), + set: (...args) => { + if (config.cookies?.disableByDefault) return; + const [key, value] = args; + persistantTestStorage.set(key, value, { expiresInDays: config.cookies?.expiresInDays }); + }, + }; + } } /** @@ -425,7 +443,9 @@ export class Abby< return persistedValue; } - + if (this.config.cookies?.disableByDefault) { + return getVariantWithHeighestWeightOrFirst(variants, weights); + } const weightedVariant = getWeightedRandomVariant(variants, weights); this.persistantTestStorage?.set(key as string, weightedVariant); @@ -531,6 +551,13 @@ export class Abby< "" ); + // special case for the cookie consent + if (testName === this.COOKIE_CONSENT_KEY) { + this.config.cookies ??= {}; + this.config.cookies.disableByDefault = cookieValue !== "true"; + return; + } + this.testOverrides.set(testName as TestName, cookieValue); this.persistantTestStorage?.set(testName as TestName, cookieValue); } @@ -604,4 +631,32 @@ export class Abby< value: RemoteConfigValueStringToType; }>; } + + /** + * Enables the usage of cookies for the storage of user data + * and also sets the cookies if possible + */ + enableCookies() { + this.config.cookies ??= {}; + this.config.cookies.disableByDefault = false; + this.persistantTestStorage?.set(this.COOKIE_CONSENT_KEY, "true"); + + Object.keys(this.#data.tests).forEach((testName) => { + this.persistantTestStorage?.set(testName, this.getTestVariant(testName as TestName)); + }); + } + + /** + * Disables the usage of cookies for storage of user data + * and also removes all set cookies if possible + */ + disableCookies() { + this.config.cookies ??= {}; + this.config.cookies.disableByDefault = true; + this.persistantTestStorage?.set(this.COOKIE_CONSENT_KEY, "false"); + + Object.keys(this.#data.tests).forEach((testName) => { + this.persistantTestStorage?.set(testName, this.getTestVariant(testName as TestName)); + }); + } } diff --git a/packages/core/src/mathHelpers.ts b/packages/core/src/mathHelpers.ts index 4956a65b..347cae41 100644 --- a/packages/core/src/mathHelpers.ts +++ b/packages/core/src/mathHelpers.ts @@ -12,9 +12,7 @@ function getRandomDecimal() { return Math.random(); } -function getWeightedRandomNumber>( - spec: T -): keyof T { +function getWeightedRandomNumber>(spec: T): keyof T { let i: keyof T; let sum = 0; const r = getRandomDecimal(); @@ -27,17 +25,13 @@ function getWeightedRandomNumber>( return i!; } -function getDefaultWeights>( - variants: Variants -) { - return Array.from({ length: variants.length }).fill( - 1 / variants.length - ); +function getDefaultWeights>(variants: Variants) { + return Array.from({ length: variants.length }).fill(1 / variants.length); } export function validateWeights< Variants extends ReadonlyArray, - Weights extends Array + Weights extends Array, >(variants: Variants, weights?: Weights): Weights { const sum = weights?.reduce((acc, weight) => acc + weight, 0); return weights != null && sum === 1 && variants.length === weights.length @@ -45,14 +39,34 @@ export function validateWeights< : (getDefaultWeights(variants) as Weights); } -export function getWeightedRandomVariant< - Variants extends ReadonlyArray ->(variants: Variants, weights?: Array): Variants[number] { +export function getWeightedRandomVariant>( + variants: Variants, + weights?: Array +): Variants[number] { const validatedWeights = validateWeights(variants, weights); return getWeightedRandomNumber( - variants.reduce((acc, variant, index) => { - acc[variant as Variants[number]] = validatedWeights[index]; - return acc; - }, {} as Record) + variants.reduce( + (acc, variant, index) => { + acc[variant as Variants[number]] = validatedWeights[index]; + return acc; + }, + {} as Record + ) ); } + +export function getVariantWithHeighestWeightOrFirst>( + variants: Variants, + weights?: Array +): Variants[number] { + const validatedWeights = validateWeights(variants, weights); + let variantWithHeighestWeight = variants[0]; + + for (let i = 1; i < variants.length; i++) { + if (validatedWeights[i] > validatedWeights[i - 1]) { + variantWithHeighestWeight = variants[i]; + } + } + + return variantWithHeighestWeight; +} diff --git a/packages/core/src/shared/interfaces.ts b/packages/core/src/shared/interfaces.ts index 6df9d679..87d0b704 100644 --- a/packages/core/src/shared/interfaces.ts +++ b/packages/core/src/shared/interfaces.ts @@ -1,5 +1,9 @@ +export interface StorageServiceOptions { + expiresInDays?: number; +} + export interface IStorageService { get(projectId: string, key: string): string | null; - set(projectId: string, key: string, value: string): void; + set(projectId: string, key: string, value: string, options?: StorageServiceOptions): void; remove(projectId: string, key: string): void; } diff --git a/packages/next/CHANGELOG.md b/packages/next/CHANGELOG.md index d5e8cff2..2d1b0f6d 100644 --- a/packages/next/CHANGELOG.md +++ b/packages/next/CHANGELOG.md @@ -1,5 +1,12 @@ # @tryabby/next +## 5.1.1 + +### Patch Changes + +- Updated dependencies [31d14b7] + - @tryabby/react@5.2.0 + ## 5.1.0 ### Minor Changes diff --git a/packages/next/package.json b/packages/next/package.json index f8a72ed0..d22f2ed8 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -1,6 +1,6 @@ { "name": "@tryabby/next", - "version": "5.1.0", + "version": "5.1.1", "description": "", "main": "dist/index.js", "files": [ diff --git a/packages/node/CHANGELOG.md b/packages/node/CHANGELOG.md index 235a3115..35f9b5c1 100644 --- a/packages/node/CHANGELOG.md +++ b/packages/node/CHANGELOG.md @@ -1,5 +1,12 @@ # @tryabby/node +## 5.1.6 + +### Patch Changes + +- Updated dependencies [31d14b7] + - @tryabby/core@5.3.0 + ## 5.1.5 ### Patch Changes diff --git a/packages/node/package.json b/packages/node/package.json index 8c5d0685..1d2c94c4 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@tryabby/node", - "version": "5.1.5", + "version": "5.1.6", "description": "", "main": "dist/index.js", "files": [ diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md index e8f4bcce..c9c7edbf 100644 --- a/packages/react/CHANGELOG.md +++ b/packages/react/CHANGELOG.md @@ -1,5 +1,16 @@ # @tryabby/react +## 5.2.0 + +### Minor Changes + +- 31d14b7: add cookie consent options + +### Patch Changes + +- Updated dependencies [31d14b7] + - @tryabby/core@5.3.0 + ## 5.1.0 ### Minor Changes diff --git a/packages/react/package.json b/packages/react/package.json index 263e0716..6775d2e1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@tryabby/react", - "version": "5.1.0", + "version": "5.2.0", "description": "", "main": "dist/index.js", "files": [ diff --git a/packages/react/src/StorageService.ts b/packages/react/src/StorageService.ts index db135f7a..5e752128 100644 --- a/packages/react/src/StorageService.ts +++ b/packages/react/src/StorageService.ts @@ -4,8 +4,11 @@ import { getRCStorageKey, type IStorageService, } from "@tryabby/core"; +import type { StorageServiceOptions } from "@tryabby/core/dist/shared/interfaces"; import Cookie from "js-cookie"; +const DEFAULT_COOKIE_AGE = 365; + class ABStorageService implements IStorageService { get(projectId: string, testName: string): string | null { const retrievedValue = Cookie.get(getABStorageKey(projectId, testName)); @@ -14,8 +17,10 @@ class ABStorageService implements IStorageService { return retrievedValue; } - set(projectId: string, testName: string, value: string): void { - Cookie.set(getABStorageKey(projectId, testName), value); + set(projectId: string, testName: string, value: string, options?: StorageServiceOptions): void { + Cookie.set(getABStorageKey(projectId, testName), value, { + expires: options?.expiresInDays ? options.expiresInDays : DEFAULT_COOKIE_AGE, + }); } remove(projectId: string, testName: string): void { @@ -32,7 +37,7 @@ class FFStorageService implements IStorageService { } set(projectId: string, flagName: string, value: string): void { - Cookie.set(getFFStorageKey(projectId, flagName), value); + Cookie.set(getFFStorageKey(projectId, flagName), value, { expires: DEFAULT_COOKIE_AGE }); } remove(projectId: string, flagName: string): void { @@ -47,7 +52,7 @@ class RCStorageService implements IStorageService { } set(projectId: string, key: string, value: string): void { - Cookie.set(getRCStorageKey(projectId, key), value); + Cookie.set(getRCStorageKey(projectId, key), value, { expires: DEFAULT_COOKIE_AGE }); } remove(projectId: string, key: string): void { diff --git a/packages/react/src/context.tsx b/packages/react/src/context.tsx index f1e4e2b1..31c27a7e 100644 --- a/packages/react/src/context.tsx +++ b/packages/react/src/context.tsx @@ -51,7 +51,7 @@ export function createAbby< return TestStorageService.get(abbyConfig.projectId, key); }, set: (key: string, value: any) => { - if (typeof window === "undefined") return; + if (typeof window === "undefined" || config.cookies?.disableByDefault) return; TestStorageService.set(abbyConfig.projectId, key, value); }, }, diff --git a/packages/remix/CHANGELOG.md b/packages/remix/CHANGELOG.md index 4efa798d..949aea79 100644 --- a/packages/remix/CHANGELOG.md +++ b/packages/remix/CHANGELOG.md @@ -1,5 +1,12 @@ # @tryabby/next +## 1.0.2 + +### Patch Changes + +- Updated dependencies [31d14b7] + - @tryabby/react@5.2.0 + ## 1.0.1 ### Patch Changes diff --git a/packages/remix/package.json b/packages/remix/package.json index 786a7c97..a5e079ab 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@tryabby/remix", - "version": "1.0.1", + "version": "1.0.2", "description": "", "main": "dist/index.js", "files": [ diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 13c288d2..94a33190 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,16 @@ # @tryabby/svelte +## 2.2.0 + +### Minor Changes + +- 31d14b7: add cookie consent options + +### Patch Changes + +- Updated dependencies [31d14b7] + - @tryabby/core@5.3.0 + ## 2.1.5 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 10a4d34a..3a07efda 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@tryabby/svelte", - "version": "2.1.5", + "version": "2.2.0", "main": "dist/index.umd.cjs", "homepage": "https://docs.tryabby.dev", "type": "module", diff --git a/packages/svelte/src/lib/StorageService.ts b/packages/svelte/src/lib/StorageService.ts index 9968f70e..c1488d1d 100644 --- a/packages/svelte/src/lib/StorageService.ts +++ b/packages/svelte/src/lib/StorageService.ts @@ -4,6 +4,7 @@ import { getRCStorageKey, type IStorageService, } from "@tryabby/core"; +import type { StorageServiceOptions } from "@tryabby/core/dist/shared/interfaces"; import Cookie from "js-cookie"; class ABStorageService implements IStorageService { @@ -14,10 +15,11 @@ class ABStorageService implements IStorageService { return retrievedValue; } - set(projectId: string, testName: string, value: string): void { - Cookie.set(getABStorageKey(projectId, testName), value); + set(projectId: string, testName: string, value: string, options?: StorageServiceOptions): void { + Cookie.set(getABStorageKey(projectId, testName), value, { + expires: options?.expiresInDays ? options.expiresInDays : 365, + }); } - remove(projectId: string, testName: string): void { Cookie.remove(getABStorageKey(projectId, testName)); }