Shared SDK: The NinetailedApiClient

Work with events and types common to all Ninetailed implementations.

The Shared SDK exposes Ninetailed data objects, types, and methods applicable across runtimes. The Shared SDK allows you to create an API Client that facilitates constructing and sending Experience API events. It also handles retrying requests and request errors.

Elements of the shared SDK are composed to create the JavaScript SDK, which is then composed into Ninetailed's React-based SDKs.

Want to browse the code? Check out the shared SDK in our open source repository.

Instantiation

import { NinetailedApiClient, fetchImpl} from "@ninetailed/experience.js-shared"

const ninetailedApiClient = new NinetailedApiClient({ clientId, environment, url, fetchImpl, }: NinetailedApiClientOptions)

type NinetailedApiClientOptions = {
    clientId: string;
    environment?: string;
    url?: string;
    fetchImpl?: FetchImpl;
}
ParameterType SignatureDescription

clientId

clientId: string

The organization ID/API key of a Ninetailed account.

environment

environment?: string

The environment key of a Ninetailed account. Typically either main or development, depending on the environment in which you have configured your content source connections. your you have configured Defaults to main if unsupplied.

url

url?: string

Specify the base URL of the Experience API. This should usually be left unspecified. Defaults to the production Experience API base URL of https://experience.ninetailed.co when unspecified.

fetchImpl

fetchImpl?: FetchImpl

A NinetailedApiClient can be used in different JavaScript runtimes including the browser, Node.js, and edge workers. However, a implementation of the fetch() method available in the browser may not be available within all of those run times. This option allows you to provide a fetch() implementation of your own should the runtime not expose one automatically.

Profile Methods

A NinetailedAPIClient maps one function for each Experience API endpoint. upsertProfile is also provided to conveniently switch between create and update functions.

MethodType SignatureDescription

upsertProfile

upsertProfile({ profileId, events }: { profileId?: string; events: Event[];} options?: RequestOptions): Promise<ProfileWithSelectedVariants>

If a profile ID is not supplied, calls updateProfile. Otherwise, calls createProfile.

createProfile

createProfile({ events }: { events: Event[];}, options?: RequestOptions): Promise<ProfileWithSelectedVariants>

Creates a profile with the specified profile ID. Interfaces with the create profile endpoint.

updateProfile

createProfile({ profileId, events }: { profileId: string, events: Event[];}, options?: RequestOptions): Promise<ProfileWithSelectedVariants>

Creates a profile at the specified profile ID. Interfaces with the update profile endpoint.

getProfile

getProfile(id: string, options?: Omit<RequestOptions, 'preflight' | 'plaintext'> ): Promise<ProfileWithSelectedVariants>

Get the profile with the specified profile ID. Interfaces with the get profile endpoint.

upsertManyProfiles

upsertManyProfiles({events} : {events: Event[], options?: { timeout?: number | undefined; enabledFeatures?: Feature[] | undefined; }): Promise<ProfileWithSelectedVariants[]>

Supply an array of events to update many profiles at once. Responds with the representation of each upserted profile. Interfaces with the batch upsert profile endpoint.

The returned response of each method is the same as that of the Experience API; a data structure indicating the complete representation of the profile(s) and the Ninetailed Experiences & variants that the Experience API has selected for the profile(s).

Request Options

All of profile methods accept request options in addition to the supplied events. These are used to control request timeout & retry behaviour, performance, localization, and location resolution.

type RequestOptions = {
    /**
     * A timeout after which a request will get cancelled
     */
    timeout?: number;
    /**
     * Return a profile as though events have been submitted without actually storing the profile state
     * Useful in ESR or SSR contexts
     */
    preflight?: boolean;
    /**
     * Determines the locale to which to localize location.city & location.country properties
     */
    locale?: string;
    /**
     * An IP address to override the IP lookup behaviour, if using also supplying "ip-enrichment" as an enabled feature
     * Used in ESR or SSR environments to proxy a client's IP instead of using the server's IP
     */
    ip?: string;
    /**
     * Sending requests as plain-text bypasses the need for a CORS preflight request, making for a faster request.
     */
    plainText?: boolean;
    /**
     * The maximum amount of retries for a request.
     * Only 503 errors will be retried. The Ninetailed API is aware of which requests are retryable and send a 503 error.
     *
     * @default 1
     */
    retries?: number;
    /**
     * The maximum amount of time in ms to wait between retries.
     * By default the retry will be sent immediately as the Ninetailed API is serverless and sends a 503 error if it could not recover a request internally.
     *
     * @default 0 (no delay)
     */
    minRetryTimeout?: number;
    /**
     * Activated features which the API should use for this request.
     */
    enabledFeatures?: Feature[];
};

/**
 * "location" = Attempt to resolve the location of the user based on the IP of the request and where it ingresses to the Experience API
 * "ip-enrichment" = Attempt to resolve firmographic data with a connected Albacross API Key. See Setup => Customer Data => Albacross
 */
type Feature = "location" | "ip-enrichment"

Event Building Functions

Each of the profile methods above accepts an array of events. Event building helper functions facilitate generating the required payload for page, track, and identify events. These builder methods take care of parsing the supplied ctx.url and populating context properties on events commonly used by Audience rules, including context.page and context.campaign.

function buildPageEvent(data: {
    messageId: string; // A UUID
    timestamp: number; // Typically assigned as Date.now()
    ctx: {
        url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters
        referrer: string;
        locale: string;
        userAgent: string;
        document?: {
            title: string;
        } | undefined;
    }
    // Optionally supply an object to merge with API-resolved location
    location?: Geolocation;
    properties: Record<string, any>; // JSON
})

function buildTrackEvent(data: {
    messageId: string; // A UUID
    timestamp: number; // Typically assigned as Date.now()
    ctx: {
        url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters
        referrer: string;
        locale: string;
        userAgent: string;
        document?: {
            title: string;
        } | undefined;
    }
    // Optionally supply an object to merge with API-resolved location
    location?: Geolocation;
    event: string; // the name of the event
    properties: Record<string, any>; // JSON
})

function buildIdentifyEvent(data: {
    messageId: string; // A UUID
    timestamp: number; // Typically assigned as Date.now()
    ctx: {
        url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters
        referrer: string;
        locale: string;
        userAgent: string;
        document?: {
            title: string;
        } | undefined;
    }
    // Optionally supply an object to merge with API-resolved location
    location?: Geolocation;
    userId: string; // An alias. If you don't want to set one, use an empty string ""
    traits: Record<string, any>; // JSON
})

type Geolocation = {
      coordinates?: {
        latitude: number;
        longitude: number;
      },
      city?: string; // Use proper capitalization of the city name
      postalCode?: string;
      region?: string;
      regionCode?: string; // ISO 3166-2
      country?: string;
      countryCode?: string; // ISO 3166-2
      continent?: string;
      timezone?: string;
}

Example Shared SDK Usage

This is a short example of using the buildPageEvent helper to create a well-formatted event of type page and using it to upsert a profile.

Middleware Example
import {buildPageEvent, buildTrackEvent, buildIdentifyEvent, NinetailedApiClient, NINETAILED_ANONYMOUS_ID_COOKIE } from "@ninetailed/experience.js-shared"

import { v4 as uuidv4 } from 'uuid';

const clientID = "YOUR_NINETAILED_CLIENT_ID" || undefined;
const apiClient = new NinetailedApiClient({ clientId });

const middleware = (req: Request) => {
  buildPageEvent({
    // Hardcoded strings are for example purposes
    // You'd populate these from the incoming Request object
    ctx: { 
      url: 'https://www.example.com/path/?query=test'
      locale: 'en-US',
      referrer: 'https://www.google.com/',
      userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
    }
    messageId: uuidv4(),
    timestamp: Date.now(),
    // Resolve your original request geolocation info and pass it along
    location: {
      city: request.geo?.city,
      region: request.geo?.region,
      country: request.geo?.country,
      continent: requqest.geo?.continent,
    } 
    properties: {},
  });
  
  const apiResponse = await apiClient.upsertProfile(
    {
      // Use cookies to store and read a Ninetailed profile ID
      // `upsertProfile` takes care of case where this isn't present
      profileId: req.cookies.get(NINETAILED_ANONYMOUS_ID_COOKIE)?.value 
      events: [{ pageEvent }],
    },
    { ip: req.ip } // Pass original IP address
  );
  
  // If no profile ID cookie present, be sure to write it in a response
  return apiResponse;
}

Despite the brevity of this example, this is all it takes to get started working with Ninetailed in edge functions and middleware.

Last updated