Node.js

This SDK can be used both on the server and in the browser. If you are using a frontend framework like React or Vue you probably want to use the specialized SDK which uses this package under the hood.

Installation

Add the client to your dependencies:

npm i tggl-client

Quick start

There are two ways to evaluate flags: stateful and stateless.

Stateful flags evaluation stores result in the client itself. This is commonly used when instantiating a new client per HTTP request or when working on the frontend. You can use isActive and get on the client to access results:

import { TgglClient } from 'tggl-client'
 
const client = new TgglClient('YOUR_API_KEY')
 
await client.setContext({
  userId: 'foo',
  email: 'foo@gmail.com',
  country: 'FR',
  // ...
})
 
if (client.isActive('my-feature')) {
  // ...
}
 
if (client.get('my-feature') === 'Variation A') {
  // ...
}

Stateless flags evaluation does not change the client state. It is commonly used on the backend with a global singleton client. You can use isActive and get on the response to access results:

const flags = await client.evalContext({
  userId: 'foo',
  email: 'foo@gmail.com',
  country: 'FR',
  // ...
})
 
if (flags.isActive('my-feature')) {
  // ...
}
 
if (flags.get('my-feature') === 'Variation A') {
  // ...
}

Differences between isActive and get

By design, inactive flags are not in the response, which means that you have no way of telling apart those cases:

  • The flag is inactive due to some conditions
  • The flag does not exist
  • The flag was deleted
  • The API key is not valid
  • Some network error

This design choice prevents unforeseen event from breaking your app, like someone deleting a flag or messing up the API key rotation. Your app will simply consider any flag to be inactive.

isActive will return true if the flag exists and is active regardless of its value, and there is no network error.

get gives you the actual value of an active flag, and this value may be "falsy" (null, false, 0, or empty string). This may lead to unforeseen behavior if you are not careful:

if (client.get('my-feature')) {
  // If 'my-feature' is active, but its value is falsy this block won't be executed
}
 
if (client.isActive('my-feature')) {
  // Even if 'my-feature' has a falsy value, this block will be executed
}

In both cases, if the flag is explicitly inactive, or if there is a network error, the block won't be executed.

Hard-coded fallback values

When using get, you can provide a fallback value that will be returned if the flag is inactive, does not exist, or in case of network error:

if (client.get('my-feature', 'Variation A') === 'Variation A') {
  // This code will be executed if 'my-feature' is either:
  // - active and explicitly equal to 'Variation A'
  // - inactive or network error
}

Having the possibility of providing a fallback value is useful when you want to have a default behavior when a flag is deleted or when the user encounters a network error:

if (client.get('my-feature', true)) {
  // This block will be executed if 'my-feature' is explicitly active or if there is a network error
}
 
if (client.isActive('my-feature')) {
  // This block will only be executed if there is no network error and the flag is explicitly active
}

Typing

CLI

Using the Tggl CLI you can run an introspection query to generate the TypeScript types for your flags and context.

# Install the CLI once
npm i --save-dev tggl-cli
 
# Generate the types every time that it is needed
tggl typing -k <SERVER_API_KEY> -o src/tggl.d.ts
 
# Drop the -k option if you have the TGGL_API_KEY environment variable set
tggl typing -o src/tggl.d.ts

Replace <SERVER_API_KEY> with your server API key or use the TGGL_API_KEY environment variable and omit the -k option. You should run this command everytime you need to update the typing. Your IDE will now autocomplete and type-check the context properties and all flag names and values.

All context properties are required except properties that you have hidden. You can also use the -h option to remove hidden properties from the context.

OptionDescription
-k, --api-key <key>Tggl API key, defaults to TGGL_API_KEY environment variable
-o, --output <file>File to write the typing to, *.d.ts to override the package typing, *.ts to simply generate interfaces
-h, --skip-hiddenSkip hidden properties in context (default: false)
-p, --package <package>Name of the package to declare types for (default: "tggl-client")
--helpdisplay help for command
Autocomplete client

Typing system

The CLI generate two interfaces: TgglContext and TgglFlags that look like this (based on your own configuration on Tggl):

interface TgglContext {
  userId: string
  email: string
  timestamp: string | number
  environement: "production" | "local" | "staging"
}
 
interface TgglFlags {
  new_blog_layout: true
  color_button: "#00ff00" | "#0000ff"
}

They are used by default by the client, but you can manually override them if you need to, notably if you want to instantiate multiple clients for different projects:

const clientOne = new TgglClient<
  FlagsProjectOne,
  ContextProjectOne
>('API_KEY_ONE')
 
const clientTwo = new TgglClient<
  FlagsProjectTwo,
  ContextProjectTwo
>('API_KEY_TWO')

The SDK also exports some helper types if needed:

import { TgglFlagSlug, TgglFlagValue, TgglFlags } from 'tggl-client'
 
// Slug type
const slug: TgglFlagSlug = 'new_blog_layout'
const slug: TgglFlagSlug<{ flag_a: true }> = 'flag_a'
 
// Value type
const value: TgglFlagValue<'color_button'> = "#00ff00"
const value: TgglFlagValue<'my_flag', { my_flag: 'a' | 'b'}> = "b"
 
// Use it to build typed functions
function getFlag<
  TFlags extends TgglFlags = TgglFlags,
  TSlug extends TgglFlagSlug<TFlags> = TgglFlagSlug<TFlags>
>(slug: TSlug): TgglFlagValue<TSlug, TFlags> | undefined {
  // ...
}

Network performance

A single API call evaluating all flags is performed when calling setContext or evalContext, making all subsequent flag checking methods synchronous and extremely fast.

This means that you do not need to cache results of isActive and get since they do not trigger an API call, they simply look up the data in the already fetched response.

Evaluating contexts in batches

If you have multiple contexts to evaluate at once, you can batch your calls in a single HTTP request for a significant performance boost:

// Responses are returned in the same order
const [ fooFlags, barFlags ] = await client.evalContexts([
  { userId: 'foo' },
  { userId: 'bar' },
])

The client uses a dataloader under the hood, which means that all calls that are performed within the same event loop are batched together:

// evalContext is called twice but a single API call is performed
const [ fooFlags, barFlags ] = await Promise.all([
  client.evalContext({ userId: 'foo' }),
  client.evalContext({ userId: 'bar' }),
])

Reporting

By default, the SDK sends flag evaluation data to Tggl to give you precious insights on how you are actually using your flags in production and to help you manage flags lifecycles better.

Monitoring 2x

The SDK sends at most one HTTP request every 2 seconds to Tggl. Additionally, you can identify the client making the request by giving it an app name. You will be able to retrieve that name on the Tggl dashboard:

const client = new TgglClient('API_KEY', {
  reporting: {
    app: 'My App:2.16.3'
  }
})

You can also disable reporting entirely:

// Disable reporting when you instantiate the client
const client = new TgglClient('API_KEY', { reporting: false })
 
// Or disable it later
const client = new TgglClient('API_KEY')
client.disableReporting()
 
// Or disable it only for one response
const client = new TgglClient('API_KEY')
const flags = client.evalContext({ userId: 'foo' })
flags.disableReporting()

Polling for "live" updates

If you need your client to be up-to-date with the latest flag configuration, you can manually call setContext or evalContext to make another API call, or simply enable polling to automatically make an API call at regular intervals:

// Update flags every 5 seconds
const client = new TgglClient('YOUR_API_KEY', {
  pollingInterval: 5000
})

Polling will only make an API call if the previous call has finished, so you don't have to worry about overlapping calls. You can also manually start and stop polling at anytime:

client.startPolling(8000) // Start polling every 8 seconds
client.startPolling(3000) // Change frequency to every 3 seconds
client.stopPolling() // Stop polling

Polling will update the internal state of the client, you can add listeners to be notified when the state changes:

const stopListener = client.onResultChange((flags) => {
  // Some flag has changed
  // Either check the flags object or use isActive/get on the client
})
 
// Call stopListener() to remove the listener
Info

onResultChange is not specific to polling, it is also called when calling setContext manually.

Using the Proxy

By default, the SDK talks directly to the Tggl API. If you are using the Tggl Proxy, you can specify the proxy URL when instantiating the client:

const client = new TgglClient('YOUR_API_KEY', {
  baseUrl: 'http://your-proxy-domain.com'
})

The /flags and /report path will be appended to the baseUrl and both flags evaluation and reporting will go through the proxy. If your proxy is configured with custom paths, you can specify them:

const client = new TgglClient('YOUR_API_KEY', {
  url: 'http://your-proxy-domain.com/custom-flags',
  reporting: {
    url: 'http://your-proxy-domain.com/custom-report'
  }
})

Error handling

Calling setContext will never throw an error even if the API call is not successful. Instead, the client internal state will not change, leaving all flags as-is. This is also true for evalContext, it will return a response object with all flags disabled.

This behavior ensures that your app will never crash because of a flag evaluation, even if the API is down or if you have a typo in your API key. You can listen for success and errors to handle them as you wish:

client.onFetchSuccessful(() => { /* ... */ })
 
client.onFetchFail((error) => { /* ... */ })

Evaluating flags locally

It is possible to evaluate flags locally on the server but not recommended unless you have performance issues evaluating flags at a high frequency, or if you need to split traffic on the edge without doing an API call. Evaluating flags locally forces you to maintain the copy of flags configuration up to date and might be a source of issues.

Danger

Make sure to add the right keys to your context to be perfectly consistent with the Tggl API.

import { TgglLocalClient } from 'tggl-client'
 
const client = new TgglLocalClient('YOUR_SERVER_API_KEY')
 
// This method performs an API call and updates the flags configuration
await client.fetchConfig()
 
// Evaluation is performed locally
client.isActive({ userId: 'foo' }, 'my-feature')
client.isActive({ userId: 'bar' }, 'my-feature')
 
// You can also get the value of a flag, with and without default value
client.get({ userId: 'baz' }, 'my-feature')
client.get({ userId: 'foobar' }, 'my-feature', 42)

When evaluating flags locally it is your responsibility to keep the configuration up to date by calling fetchConfig when needed. You can use webhooks to be notified when the configuration changes or simply poll the API at regular intervals:

const client = new TgglLocalClient('YOUR_SERVER_API_KEY', {
  pollingInterval: 5000
})

You can cache the configuration and instantiate the client with the cached version, so you don't need to call fetchConfig:

import { TgglLocalClient } from 'tggl-client'
 
const cachedConfig = await loadConfig()
 
const client = new TgglLocalClient('YOUR_SERVER_API_KEY', {
  initialConfig: cachedConfig
})
 
client.onConfigChange(async (config) => {
  await saveConfig(config)
})

onConfigChange is called only if the configuration changes when calling fetchConfig manually or when polling, but not when calling setConfig manually.

Alternatively, you can use the Proxy to handle caching and redundancy for you. This can even reduce costs as less calls will reach the API.

const client = new TgglLocalClient('YOUR_SERVER_API_KEY', {
  baseUrl: 'http://your-proxy-domain.com'
})
 
// Will fetch config from the proxy
await client.fetchConfig()