@avsbhq/vue 1.0.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 +363 -0
- package/dist/index.cjs +522 -0
- package/dist/index.d.cts +279 -0
- package/dist/index.d.ts +279 -0
- package/dist/index.js +519 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# @avsbhq/vue
|
|
2
|
+
|
|
3
|
+
Vue 3 SDK for the [A vs B](https://app.avsb.cloud) platform.
|
|
4
|
+
|
|
5
|
+
Drop an `<AvsbProvider>` at the top of your component tree (or call `app.use(AvsbPlugin)`) and read feature flags from anywhere using composables. Built on `@avsbhq/browser`; components re-render automatically when flag values change.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @avsbhq/vue
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Vue 3.4 or later is required as a peer dependency. Pinia integration is optional — install `pinia` only if you use `useAvsbStore`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. Quickstart
|
|
20
|
+
|
|
21
|
+
```vue
|
|
22
|
+
<!-- App.vue -->
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { AvsbProvider } from '@avsbhq/vue'
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<AvsbProvider :sdk-key="sdkKey" :context="{ kind: 'user', key: userId }">
|
|
29
|
+
<RouterView />
|
|
30
|
+
</AvsbProvider>
|
|
31
|
+
</template>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<!-- CheckoutButton.vue -->
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { useFlag, useTrack } from '@avsbhq/vue'
|
|
38
|
+
|
|
39
|
+
const newCheckout = useFlag('new-checkout-flow', false)
|
|
40
|
+
const track = useTrack()
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<template>
|
|
44
|
+
<button @click="track('checkout_clicked')">
|
|
45
|
+
{{ newCheckout.value ? 'Start checkout (new)' : 'Buy now' }}
|
|
46
|
+
</button>
|
|
47
|
+
</template>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 3. SDK keys
|
|
53
|
+
|
|
54
|
+
Get a client SDK key from your A vs B project's Environments page.
|
|
55
|
+
|
|
56
|
+
Client SDK keys are safe to embed in browser bundles — store them in your framework's public env variable:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
VITE_AVSB_SDK_KEY=sdk-...
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// main.ts
|
|
64
|
+
const key = import.meta.env.VITE_AVSB_SDK_KEY
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Server SDK keys (used by `@avsbhq/node`) must never be exposed to the browser.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 4. Identity — identify, updateAttributes, alias, reset
|
|
72
|
+
|
|
73
|
+
### Provider-level context
|
|
74
|
+
|
|
75
|
+
Pass a context object to `<AvsbProvider>` at bootstrap. This is the evaluation context used for all flag reads until you call `identify`.
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<AvsbProvider :sdk-key="key" :context="{ kind: 'user', key: userId, plan: 'pro' }">
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### useIdentify
|
|
82
|
+
|
|
83
|
+
Replace the current context at runtime (e.g. after sign-in):
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { useIdentify } from '@avsbhq/vue'
|
|
87
|
+
|
|
88
|
+
const identify = useIdentify()
|
|
89
|
+
|
|
90
|
+
function onSignIn(user: User) {
|
|
91
|
+
identify({ kind: 'user', key: user.id, email: user.email, plan: user.plan })
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The function is stable — it will not change reference between renders.
|
|
96
|
+
|
|
97
|
+
### useAlias
|
|
98
|
+
|
|
99
|
+
Record that an anonymous visitor and an identified user are the same person. Call this once at sign-in:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { useAlias } from '@avsbhq/vue'
|
|
103
|
+
|
|
104
|
+
const alias = useAlias()
|
|
105
|
+
|
|
106
|
+
async function onSignIn(anonKey: string, userId: string) {
|
|
107
|
+
await alias(
|
|
108
|
+
{ kind: 'user', key: anonKey },
|
|
109
|
+
{ kind: 'user', key: userId }
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### reset
|
|
115
|
+
|
|
116
|
+
Call `client.reset()` via `useAvsbClient()` to clear the current identity and return to the anonymous state.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 5. Multi-context
|
|
121
|
+
|
|
122
|
+
A vs B supports multi-context evaluation — evaluating a flag for both a user and an organisation simultaneously:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
identify({
|
|
126
|
+
kind: 'multi',
|
|
127
|
+
user: { kind: 'user', key: 'u_123', plan: 'pro' },
|
|
128
|
+
organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## 6. Reading flags
|
|
135
|
+
|
|
136
|
+
### useFlag — full Flag object
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { useFlag } from '@avsbhq/vue'
|
|
140
|
+
|
|
141
|
+
const checkoutFlag = useFlag('new-checkout-flow', false)
|
|
142
|
+
// checkoutFlag is ComputedRef<Flag<boolean>>
|
|
143
|
+
|
|
144
|
+
// In template:
|
|
145
|
+
// checkoutFlag.value.value → the boolean value
|
|
146
|
+
// checkoutFlag.value.isEnabled() → true if rule-served AND truthy
|
|
147
|
+
// checkoutFlag.value.variationKey → 'on' | 'off' | ...
|
|
148
|
+
// checkoutFlag.value.source → 'rule' | 'override' | 'not_found' | ...
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
A default value is always required. It is returned until the SDK is ready and whenever the flag is not found.
|
|
152
|
+
|
|
153
|
+
### useFlagValue — raw value shortcut
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { useFlagValue } from '@avsbhq/vue'
|
|
157
|
+
|
|
158
|
+
const showBanner = useFlagValue('promo-banner', false)
|
|
159
|
+
// showBanner is ComputedRef<boolean>
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Typed variants
|
|
163
|
+
|
|
164
|
+
Use typed composables when you want a runtime type-safety check against the datafile's declared flag type:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import { useBoolFlag, useStringFlag, useNumberFlag, useJsonFlag } from '@avsbhq/vue'
|
|
168
|
+
|
|
169
|
+
const darkMode = useBoolFlag('dark-mode', false)
|
|
170
|
+
const theme = useStringFlag('theme', 'light')
|
|
171
|
+
const maxItems = useNumberFlag('max-items', 10)
|
|
172
|
+
const config = useJsonFlag<{ timeout: number }>('api-config', { timeout: 5000 })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
All return `ComputedRef<Flag<T>>`.
|
|
176
|
+
|
|
177
|
+
### Reactive flag key
|
|
178
|
+
|
|
179
|
+
`useFlag` (and `useFlagValue`) accept a `Ref<string>` or `ComputedRef<string>` as the key — the subscription updates automatically when the key changes:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
const selectedFlag = ref('feature-a')
|
|
183
|
+
const flag = useFlag(selectedFlag, false)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### useAllFlags — all evaluated flags
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import { useAllFlags } from '@avsbhq/vue'
|
|
190
|
+
|
|
191
|
+
const flags = useAllFlags()
|
|
192
|
+
// flags is ComputedRef<Record<string, Flag>>
|
|
193
|
+
// Re-computes on any flagChange event.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Prefer individual `useFlag` composables in most components — `useAllFlags` triggers on any flag change and is best suited for debug panels.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 7. Tracking events
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { useTrack } from '@avsbhq/vue'
|
|
204
|
+
|
|
205
|
+
const track = useTrack()
|
|
206
|
+
|
|
207
|
+
function onClick() {
|
|
208
|
+
track('button_clicked', { location: 'header', variant: 'A' })
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
The function is defined once in setup — it is safe to use as an event handler without wrapping in a closure.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 8. Error handling
|
|
217
|
+
|
|
218
|
+
### Provider-level error state
|
|
219
|
+
|
|
220
|
+
`<AvsbProvider>` exposes a `status` that transitions to `'error'` if `onReady()` rejects or if the client emits an `'error'` event. Use `useFlagReady` to gate rendering:
|
|
221
|
+
|
|
222
|
+
```vue
|
|
223
|
+
<script setup lang="ts">
|
|
224
|
+
import { useFlagReady } from '@avsbhq/vue'
|
|
225
|
+
const ready = useFlagReady()
|
|
226
|
+
</script>
|
|
227
|
+
|
|
228
|
+
<template>
|
|
229
|
+
<div v-if="!ready">Loading flags...</div>
|
|
230
|
+
<MyApp v-else />
|
|
231
|
+
</template>
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Custom logger
|
|
235
|
+
|
|
236
|
+
Pass a logger to suppress or redirect SDK output:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
app.use(AvsbPlugin, {
|
|
240
|
+
sdkKey: '...',
|
|
241
|
+
logger: {
|
|
242
|
+
debug: (msg, ...args) => myLogger.debug(msg, ...args),
|
|
243
|
+
warn: (msg, ...args) => myLogger.warn(msg, ...args),
|
|
244
|
+
error: (msg, ...args) => myLogger.error(msg, ...args),
|
|
245
|
+
},
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## 9. SSR / Nuxt
|
|
252
|
+
|
|
253
|
+
For SSR, pre-fetch the datafile server-side and pass it as the `bootstrap` option to skip the initial network round-trip on the client:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
// server.ts
|
|
257
|
+
import { AvsbClient } from '@avsbhq/browser'
|
|
258
|
+
|
|
259
|
+
const client = new AvsbClient({ sdkKey: '...', bootstrap: serverDatafile })
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```vue
|
|
263
|
+
<!-- App.vue — client side -->
|
|
264
|
+
<AvsbProvider :sdk-key="sdkKey" :bootstrap="bootstrapDatafile">
|
|
265
|
+
<NuxtPage />
|
|
266
|
+
</AvsbProvider>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Flag composables return a `not_found` sentinel during the hydration window so SSR output matches the client's initial render.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 10. Graceful shutdown
|
|
274
|
+
|
|
275
|
+
When using `<AvsbProvider>` in Mode A (sdkKey prop), the client is closed automatically on component unmount. This flushes any queued events.
|
|
276
|
+
|
|
277
|
+
When using Mode B (passing a pre-built `client` prop), you own the lifecycle:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
onUnmounted(async () => {
|
|
281
|
+
await client.flush()
|
|
282
|
+
await client.close()
|
|
283
|
+
})
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The `AvsbPlugin` (Mode A) proxies `app.unmount` to close the client when the Vue app tears down.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## 11. Testing
|
|
291
|
+
|
|
292
|
+
Inject a mock context directly via `provide` — no need to mount the full provider:
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
import { defineComponent, h, provide } from 'vue'
|
|
296
|
+
import { mount } from '@vue/test-utils'
|
|
297
|
+
import { vi } from 'vitest'
|
|
298
|
+
import { AvsbInjectionKey } from '@avsbhq/vue'
|
|
299
|
+
import { notFoundFlag, createFlag } from '@avsbhq/core'
|
|
300
|
+
|
|
301
|
+
function makeMockClient() {
|
|
302
|
+
return {
|
|
303
|
+
subscribe: vi.fn((_key, cb) => { /* store cb */ return () => {} }),
|
|
304
|
+
getSnapshot: vi.fn((key, def) => notFoundFlag(def, 'no data')),
|
|
305
|
+
on: vi.fn(() => () => {}),
|
|
306
|
+
onReady: vi.fn(() => Promise.resolve()),
|
|
307
|
+
close: vi.fn(() => Promise.resolve()),
|
|
308
|
+
track: vi.fn(),
|
|
309
|
+
identify: vi.fn(),
|
|
310
|
+
alias: vi.fn(() => Promise.resolve()),
|
|
311
|
+
reset: vi.fn(),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const Wrapper = defineComponent({
|
|
316
|
+
setup() {
|
|
317
|
+
provide(AvsbInjectionKey, {
|
|
318
|
+
client: makeMockClient(),
|
|
319
|
+
status: 'ready',
|
|
320
|
+
})
|
|
321
|
+
},
|
|
322
|
+
render() {
|
|
323
|
+
return h(MyComponent)
|
|
324
|
+
},
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
const wrapper = mount(Wrapper)
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Use `createFlag()` from `@avsbhq/core` to build typed flag fixtures:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import { createFlag } from '@avsbhq/core'
|
|
334
|
+
|
|
335
|
+
const FLAG_ON = createFlag<boolean>({
|
|
336
|
+
value: true,
|
|
337
|
+
variationKey: 'on',
|
|
338
|
+
source: 'rule',
|
|
339
|
+
ruleId: 'r1',
|
|
340
|
+
ruleType: 'ab_test',
|
|
341
|
+
reasons: ['matched rule'],
|
|
342
|
+
})
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 12. Migration from LaunchDarkly
|
|
348
|
+
|
|
349
|
+
| LaunchDarkly (React SDK) | @avsbhq/vue equivalent |
|
|
350
|
+
|---------------------------------------|----------------------------------------------------|
|
|
351
|
+
| `<LDProvider clientSideID="...">` | `<AvsbProvider sdkKey="...">` |
|
|
352
|
+
| `useLDClient()` | `useAvsbClient()` |
|
|
353
|
+
| `useFlags()` | `useAllFlags()` |
|
|
354
|
+
| `useLDFlag('key', default)` | `useFlag('key', default).value` |
|
|
355
|
+
| `ldClient.identify(context)` | `useIdentify()(context)` |
|
|
356
|
+
| `ldClient.track('event')` | `useTrack()('event')` |
|
|
357
|
+
| `ldClient.variation('key', default)` | `useAvsbClient()?.getFlag('key', default)` |
|
|
358
|
+
|
|
359
|
+
Key differences:
|
|
360
|
+
|
|
361
|
+
- `useFlag` returns a `Flag<T>` object, not a raw `T`. Access `.value` for the primitive or `.isEnabled()` for a boolean gate. This gives you the evaluation metadata (source, variationKey, reasons) without a second call.
|
|
362
|
+
- All `getFlag` variants require an explicit `defaultValue`. There is no untyped `.variation()`.
|
|
363
|
+
- Multi-context is a first-class concept — no adapter required.
|