Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/add-vue-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tryabby/vue": minor
---

Add the typed Vue 3 integration for A/B tests, feature flags, and remote config.
13 changes: 7 additions & 6 deletions packages/core/src/index.ts

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai review

Original file line number Diff line number Diff line change
Expand Up @@ -517,11 +517,12 @@ export class Abby<
}

/**
* Function to get the value of a test variant. This includes
* the overrides from the dev tools and the local overrides if in development mode
* if there is no override or a stored variant we generate a new one and store it
* @param key The name of the test
* @returns the value of the test variant
* Selects a test variant, honoring local overrides in development mode.
* Reuses a persisted value only if it remains in the configured variants.
* Otherwise selects by weight and persists the result when cookies are enabled;
* with cookies disabled, uses the highest-weight variant or the first variant.
* @param key - The configured A/B test name.
* @returns The override, valid persisted variant, or newly selected variant.
*/
getTestVariant<T extends keyof Tests>(key: T): Tests[T]["variants"][number] {
this.log("getTestVariant()", key);
Expand All @@ -538,7 +539,7 @@ export class Abby<

const persistedValue = this.persistantTestStorage?.get(key as string);

if (persistedValue != null) {
if (persistedValue != null && variants.includes(persistedValue)) {
this.log("getTestVariant() => persistedValue:", persistedValue);

return persistedValue;
Expand Down
52 changes: 52 additions & 0 deletions packages/core/tests/persisted-variants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it, vi } from "vitest";
import { Abby } from "../src/index";

function createClient(persistedValue: string | null, disableByDefault = false) {
const storage = {
get: vi.fn(() => persistedValue),
set: vi.fn(),
};
const client = new Abby(
{
projectId: "persisted-variants-test",
environments: ["test"],
currentEnvironment: "test",
cookies: { disableByDefault },
tests: { checkout: { variants: ["control", "current"] } },
},
storage
);
client.init({
flags: [],
tests: [{ name: "checkout", weights: [0, 1] }],
remoteConfig: [],
});
return { client, storage };
}

describe("persisted variant selection", () => {
it("preserves a persisted variant that is still configured", () => {
const { client, storage } = createClient("control");
expect(client.getTestVariant("checkout")).toBe("control");
expect(storage.set).not.toHaveBeenCalled();
});

it("replaces a removed persisted variant using the configured weights", () => {
const { client, storage } = createClient("removed");
expect(client.getTestVariant("checkout")).toBe("current");
expect(storage.set).toHaveBeenCalledWith("checkout", "current", {
expiresInDays: undefined,
});
});

it("ignores a removed persisted variant without writing when cookies are disabled", () => {
const { client, storage } = createClient("removed", true);
expect(client.getTestVariant("checkout")).toBe("current");
expect(storage.set).not.toHaveBeenCalled();
});

it("selects a configured variant when no persisted value exists", () => {
const { client } = createClient(null);
expect(client.getTestVariant("checkout")).toBe("current");
});
});
2 changes: 2 additions & 0 deletions packages/vue/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist
node_modules
42 changes: 42 additions & 0 deletions packages/vue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# `@tryabby/vue`

Typed Vue 3 composables for Abby A/B tests, feature flags, and remote config.

```ts
import { createAbby } from "@tryabby/vue";

export const abby = createAbby({
projectId: "project-id",
environments: ["development", "production"],
currentEnvironment: "development",
tests: { checkout: { variants: ["control", "new"] } },
flags: ["newNav"],
remoteConfig: { headline: "String" },
});
```

Wrap the application (or a Nuxt layout) in the returned provider:

```vue
<script setup lang="ts">
import { abby } from "./abby";
</script>

<template>
<abby.AbbyProvider><NuxtPage /></abby.AbbyProvider>
</template>
```

Use the typed composables in descendants. Values are Vue computed refs and are automatically unwrapped in templates.

```vue
<script setup lang="ts">
import { abby } from "./abby";

const { variant, onAct } = abby.useAbby("checkout");
const newNav = abby.useFeatureFlag("newNav");
const headline = abby.useRemoteConfig("headline");
</script>
```

For SSR, pass server-loaded `AbbyDataResponse` as `initialData`. A/B variants intentionally render empty until client mount to avoid hydration mismatches; hydrated feature flags and remote config can render on the server.
43 changes: 43 additions & 0 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "@tryabby/vue",
"version": "7.0.1",
"description": "Vue 3 integration for Abby",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/",
"dev": "pnpm run build --watch",
"test": "vitest run",
"typecheck": "tsc --noEmit"
},
"homepage": "https://docs.tryabby.com",
"license": "ISC",
"peerDependencies": {
"vue": "^3.3.0"
},
"dependencies": {
"@tryabby/core": "workspace:*",
"js-cookie": "^3.0.5"
},
"devDependencies": {
"@types/js-cookie": "^3.0.3",
"@vue/server-renderer": "3.4.24",
"@vue/test-utils": "2.4.6",
"jsdom": "^20.0.3",
"tsconfig": "workspace:*",
"tsup": "^6.5.0",
"typescript": "5.5.4",
"vite": "5.4.0",
"vitest": "2.0.5",
"vue": "3.4.24"
}
}
Loading