@avsbhq/angular
v1.0.0
Published
Angular SDK for the [A vs B](https://app.avsb.cloud) feature flag and A/B testing platform. Supports Angular 17+ with both NgModule-based and standalone bootstrap. Reactive flag reads via RxJS Observables. Includes a structural directive and a pipe for te
Downloads
171
Readme
@avsbhq/angular
Angular SDK for the A vs B feature flag and A/B testing platform. Supports Angular 17+ with both NgModule-based and standalone bootstrap. Reactive flag reads via RxJS Observables. Includes a structural directive and a pipe for template-level flag gating.
1. Install
npm install @avsbhq/angularAngular 17+, RxJS 7+, and @angular/common are peer dependencies — they must be installed separately (they almost certainly already are).
npm install @angular/core @angular/common rxjs@avsbhq/core and @avsbhq/browser ship as runtime dependencies; you do not need to install them separately.
2. Quickstart (5 min integration)
Standalone bootstrap (Angular 17+)
// main.ts
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from './app/app.component'
import { provideAvsb } from '@avsbhq/angular'
bootstrapApplication(AppComponent, {
providers: [
provideAvsb({ sdkKey: 'sdk-pub-your-key-here' }),
],
})NgModule bootstrap
// app.module.ts
import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'
import { AvsbModule } from '@avsbhq/angular'
import { AppComponent } from './app.component'
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
AvsbModule.forRoot({ sdkKey: 'sdk-pub-your-key-here' }),
],
bootstrap: [AppComponent],
})
export class AppModule {}Read a flag in a component
import { Component } from '@angular/core'
import { AsyncPipe } from '@angular/common'
import { AvsbService } from '@avsbhq/angular'
import { map } from 'rxjs'
@Component({
standalone: true,
imports: [AsyncPipe],
template: `
<button *ngIf="showNewCheckout$ | async">
New checkout flow
</button>
`,
})
export class CheckoutButtonComponent {
private readonly avsb = inject(AvsbService)
readonly showNewCheckout$ = this.avsb
.getBoolFlag('new-checkout-flow', false)
.pipe(map((f) => f.value))
}3. SDK keys
Client SDK keys are public-facing and safe to ship in Angular bundles. Obtain them from your A vs B project dashboard:
- Log in to app.avsb.cloud
- Open your project, navigate to Environments
- Copy the Client SDK key for the target environment
Store the key in your environment config:
// environment.ts
export const environment = {
avsbSdkKey: 'sdk-pub-your-key-here',
}provideAvsb({ sdkKey: environment.avsbSdkKey })4. Identity (identify, alias, reset)
import { inject } from '@angular/core'
import { AvsbService } from '@avsbhq/angular'
const avsb = inject(AvsbService)
// Set user context after login
avsb.identify({ kind: 'user', key: 'user_123', plan: 'pro', country: 'GB' })
// Alias anonymous -> authenticated (call once, at sign-in)
avsb.alias({ kind: 'user', key: 'anon_xyz' }, { kind: 'user', key: 'user_123' })
// Reset context on logout
avsb.reset()The identify call updates the evaluation context immediately. Active subscriptions re-evaluate against the new context.
5. Multi-context
Pass a MultiContext to identify when you need to target by multiple independent dimensions (user + organisation + device):
avsb.identify({
kind: 'multi',
user: { kind: 'user', key: 'user_123', plan: 'pro' },
organization: { kind: 'organization', key: 'org_456', tier: 'enterprise' },
})6. Reading flags
Service — Observable API
AvsbService is the primary interface. Inject it anywhere inside the Angular DI tree.
import { inject } from '@angular/core'
import { AvsbService } from '@avsbhq/angular'
const avsb = inject(AvsbService)
// Full Flag<T> object (value + metadata)
avsb.getFlag<string>('homepage-hero', 'control').subscribe((flag) => {
console.log(flag.value, flag.source, flag.variationKey)
})
// Typed convenience variants
avsb.getBoolFlag('dark-mode', false).subscribe((f) => { /* f.value is boolean */ })
avsb.getStringFlag('theme', 'light').subscribe((f) => { /* f.value is string */ })
avsb.getNumberFlag('max-items', 10).subscribe((f) => { /* f.value is number */ })
avsb.getJsonFlag<{ ctaText: string }>('hero-config', { ctaText: 'Buy' }).subscribe(...)
// All flags — emits on any change
avsb.getAllFlags().subscribe((flags) => { console.log(Object.keys(flags)) })All methods require a defaultValue. This value is emitted immediately on subscribe (before the SDK initialises) and whenever the flag is not found.
The Flag<T> object
Every Observable emits a Flag<T> with:
| Field | Type | Description |
|---|---|---|
| value | T | The variation value |
| variationKey | string \| null | Variation key from the datafile |
| source | EvaluationSource | Why this value was produced |
| ruleId | string \| null | The rule that matched |
| ruleType | RuleType \| null | Rule type |
| reasons | string[] | Human-readable reasons |
| isEnabled() | () => boolean | True when served by a rule and value is truthy |
| exists() | () => boolean | False only when source is 'not_found' |
Structural directive
Use *avsbFlag to conditionally render template blocks:
<!-- Render when flag.isEnabled() is true -->
<div *avsbFlag="'new-dashboard'">
New dashboard content
</div>
<!-- Render when flag.value === 'variant-a' (value-match mode) -->
<div *avsbFlag="'hero-variant'; default: 'control'; when: 'variant-a'">
Hero variant A
</div>Import AvsbFlagDirective into your standalone component or NgModule:
import { AvsbFlagDirective } from '@avsbhq/angular'
@Component({ standalone: true, imports: [AvsbFlagDirective], ... })Pipe
Use avsbFlag in templates for inline flag reads:
<!-- Returns the flag value directly (not the Flag<T> wrapper) -->
<h1>{{ 'homepage-hero' | avsbFlag : 'control' }}</h1>
<!-- With kind hint (currently informational) -->
{{ 'my-flag' | avsbFlag : false : 'bool' }}The pipe is pure: false — it subscribes lazily and calls ChangeDetectorRef.markForCheck() on updates. Import AvsbFlagPipe:
import { AvsbFlagPipe } from '@avsbhq/angular'RxJS helpers (injection-context functions)
Standalone functional helpers callable from constructors, inject(), or runInInjectionContext:
import { flag$, flagValue$, boolFlag$, flagReady$ } from '@avsbhq/angular'
// Inside a constructor or injection context:
const hero$ = flag$<string>('homepage-hero', 'control')
const isEnabled$ = boolFlag$('new-feature', false)
const isReady$ = flagReady$() // Observable<boolean>Available helpers: flag$, flagValue$, boolFlag$, stringFlag$, numberFlag$, jsonFlag$, allFlags$, flagReady$, track, identify, alias.
7. Tracking events
const avsb = inject(AvsbService)
// Simple event
avsb.track('checkout_clicked')
// With metric value and properties
avsb.track('purchase_completed', {
value: 149.99,
properties: { currency: 'GBP', productId: 'prod_123' },
})8. Error handling
const avsb = inject(AvsbService)
// Subscribe to status changes
avsb.status$.subscribe((status) => {
if (status === 'error') {
console.error('SDK error:', avsb.error$.getValue())
}
})
// Wait for ready before making decisions
avsb.onReady().then((result) => {
if (!result.success) {
// SDK initialised in degraded mode; defaults are in use
}
})Errors from the underlying client bus propagate to status$ ('error') and error$.
9. SSR / hydration
For Angular Universal (SSR), use Mode B (injection) to pass a pre-initialised server-side client:
// server.ts
import { AvsbNodeClient } from '@avsbhq/node'
import { provideAvsb } from '@avsbhq/angular'
const serverClient = new AvsbNodeClient({ sdkKey: process.env.AVSB_SERVER_KEY })
await serverClient.onReady()
bootstrapApplication(AppComponent, {
providers: [
provideAvsb({ client: serverClient }),
],
})In Mode B, the Angular service does not close the client on teardown — the server application manages the lifecycle.
10. Graceful shutdown
In Mode A (sdkKey-only config), the SDK client is closed automatically when the Angular application destroys, via ApplicationRef.onDestroy. You may also call service.destroy() explicitly:
const avsb = inject(AvsbService)
// In app teardown / NgOnDestroy:
avsb.destroy()11. Testing
Use Mode B to inject a mock client in tests:
import { TestBed } from '@angular/core/testing'
import { provideAvsb } from '@avsbhq/angular'
import { createFlag } from '@avsbhq/core'
const mockClient = {
onReady: () => Promise.resolve({ success: true, source: 'network' }),
subscribe: (key, cb) => () => {},
getSnapshot: (key, defaultValue) =>
createFlag({ value: true, variationKey: 'on', source: 'rule',
ruleId: 'r1', ruleType: 'ab_test', reasons: [] }),
// ... other methods
}
TestBed.configureTestingModule({
providers: [provideAvsb({ client: mockClient })],
})Or bypass DI entirely by constructing AvsbService directly with a mock client:
const service = new AvsbService(mockClient)
service.client$.next(mockClient)12. Migration from other tools
From LaunchDarkly
| LaunchDarkly | @avsbhq/angular |
|---|---|
| LDProvider | AvsbModule.forRoot() or provideAvsb() |
| useFlags() hook | AvsbService.getFlag() Observable |
| useLDClient() | inject(AvsbService) |
| ldClient.variation(key, default) | AvsbService.getFlag(key, default) |
| ldClient.track(key) | AvsbService.track(key) |
| ldClient.identify(context) | AvsbService.identify(context) |
The Flag<T> object has no direct LD equivalent — it carries richer metadata. Use flag.value for the variation value and flag.isEnabled() for the boolean gate pattern.
Known limitations
This package targets Angular 17+ standalone bootstrap (JIT-compatible). It ships standard ESM TypeScript output via tsup rather than ng-packagr partial-ivy output. This means:
- AOT compilation happens at the consumer's build site (not pre-compiled in this package)
- The package does not include
.ngccmetadata or partial-ivy pre-compilation artifacts - Consumers on older Angular versions (< 17) may need to verify decorator metadata emission
Future versions may switch to ng-packagr for AOT pre-compilation.
