Shared SDK

Shared SDK: The NinetailedApiClient

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.

For more information about the code, see the shared SDK in our open source repository.

Instantiation

1import { NinetailedApiClient, fetchImpl} from "@ninetailed/experience.js-shared"
2
3const ninetailedApiClient = new NinetailedApiClient({ clientId, environment, url, fetchImpl, }: NinetailedApiClientOptions)
4
5type NinetailedApiClientOptions = {
6 clientId: string;
7 environment?: string;
8 url?: string;
9 fetchImpl?: FetchImpl;
10}
ParameterType SignatureDescription
clientIdclientId: stringThe organization ID/API key of a Contentful Personalization account.
environmentenvironment?: stringThe environment key of a Contentful Personalization account. Typically either main or development, depending on the environment in which you have configured your content source connections. Defaults to main if unsupplied.
urlurl?: stringSpecify 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.
fetchImplfetchImpl?: FetchImplA NinetailedApiClient can be used in different JavaScript runtimes including the browser, Node.js, and edge workers. However, an 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
upsertProfileupsertProfile({ profileId, events }: { profileId?: string; events: Event[];} options?: RequestOptions): Promise<ProfileWithSelectedVariants>If a profile ID is not supplied, calls updateProfile. Otherwise, calls createProfile.
createProfilecreateProfile({ events }: { events: Event[];}, options?: RequestOptions): Promise<ProfileWithSelectedVariants>Creates a profile with the specified profile ID. Interfaces with the create profile endpoint.
updateProfilecreateProfile({ profileId, events }: { profileId: string, events: Event[];}, options?: RequestOptions): Promise<ProfileWithSelectedVariants>Creates a profile at the specified profile ID. Interfaces with the update profile endpoint.
getProfilegetProfile(id: string, options?: Omit<RequestOptions, 'preflight' | 'plaintext'> ): Promise<ProfileWithSelectedVariants>Get the profile with the specified profile ID. Interfaces with the get profile endpoint.
upsertManyProfilesupsertManyProfiles({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 and 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 behavior, performance, localization, and location resolution.

1type RequestOptions = {
2 /**
3 * A timeout after which a request will get cancelled
4 */
5 timeout?: number;
6 /**
7 * Return a profile as though events have been submitted without actually storing the profile state
8 * Useful in ESR or SSR contexts
9 */
10 preflight?: boolean;
11 /**
12 * Determines the locale to which to localize location.city & location.country properties
13 */
14 locale?: string;
15 /**
16 * An IP address to override the IP lookup behavior, if using also supplying "ip-enrichment" as an enabled feature
17 * Used in ESR or SSR environments to proxy a client's IP instead of using the server's IP
18 */
19 ip?: string;
20 /**
21 * Sending requests as plain-text bypasses the need for a CORS preflight request, making for a faster request.
22 */
23 plainText?: boolean;
24 /**
25 * The maximum amount of retries for a request.
26 * Only 503 errors will be retried. The Ninetailed API is aware of which requests are retryable and send a 503 error.
27 *
28 * @default 1
29 */
30 retries?: number;
31 /**
32 * The maximum amount of time in ms to wait between retries.
33 * 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.
34 *
35 * @default 0 (no delay)
36 */
37 minRetryTimeout?: number;
38 /**
39 * Activated features which the API should use for this request.
40 */
41 enabledFeatures?: Feature[];
42};
43
44/**
45 * "location" = Attempt to resolve the location of the user based on the IP of the request and where it ingresses to the Experience API
46 * "ip-enrichment" = Attempt to resolve firmographic data with a connected Albacross API Key. See Setup => Customer Data => Albacross
47 */
48type 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.

1function buildPageEvent(data: {
2 messageId: string; // A UUID
3 timestamp: number; // Typically assigned as Date.now()
4 ctx: {
5 url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters
6 referrer: string;
7 locale: string;
8 userAgent: string;
9 document?: {
10 title: string;
11 } | undefined;
12 }
13 // Optionally supply an object to merge with API-resolved location
14 location?: Geolocation;
15 properties: Record<string, any>; // JSON
16})
17
18function buildTrackEvent(data: {
19 messageId: string; // A UUID
20 timestamp: number; // Typically assigned as Date.now()
21 ctx: {
22 url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters
23 referrer: string;
24 locale: string;
25 userAgent: string;
26 document?: {
27 title: string;
28 } | undefined;
29 }
30 // Optionally supply an object to merge with API-resolved location
31 location?: Geolocation;
32 event: string; // the name of the event
33 properties: Record<string, any>; // JSON
34})
35
36function buildIdentifyEvent(data: {
37 messageId: string; // A UUID
38 timestamp: number; // Typically assigned as Date.now()
39 ctx: {
40 url: string; // Supply the whole URL, including the protocol, domain, path, and any query parameters
41 referrer: string;
42 locale: string;
43 userAgent: string;
44 document?: {
45 title: string;
46 } | undefined;
47 }
48 // Optionally supply an object to merge with API-resolved location
49 location?: Geolocation;
50 userId: string; // An alias. If you don't want to set one, use an empty string ""
51 traits: Record<string, any>; // JSON
52})
53
54type Geolocation = {
55 coordinates?: {
56 latitude: number;
57 longitude: number;
58 },
59 city?: string; // Use proper capitalization of the city name
60 postalCode?: string;
61 region?: string;
62 regionCode?: string; // ISO 3166-2
63 country?: string;
64 countryCode?: string; // ISO 3166-2
65 continent?: string;
66 timezone?: string;
67}

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.

1import {buildPageEvent, buildTrackEvent, buildIdentifyEvent, NinetailedApiClient, NINETAILED_ANONYMOUS_ID_COOKIE } from "@ninetailed/experience.js-shared"
2
3import { v4 as uuidv4 } from 'uuid';
4
5const clientID = "YOUR_NINETAILED_CLIENT_ID" || undefined;
6const apiClient = new NinetailedApiClient({ clientId });
7
8const middleware = (req: Request) => {
9 buildPageEvent({
10 // Hardcoded strings are for example purposes
11 // You'd populate these from the incoming Request object
12 ctx: {
13 url: 'https://www.example.com/path/?query=test'
14 locale: 'en-US',
15 referrer: 'https://www.google.com/',
16 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',
17 }
18 messageId: uuidv4(),
19 timestamp: Date.now(),
20 // Resolve your original request geolocation info and pass it along
21 location: {
22 city: request.geo?.city,
23 region: request.geo?.region,
24 country: request.geo?.country,
25 continent: request.geo?.continent,
26 }
27 properties: {},
28 });
29
30 const apiResponse = await apiClient.upsertProfile(
31 {
32 // Use cookies to store and read a Ninetailed profile ID
33 // `upsertProfile` takes care of case where this isn't present
34 profileId: req.cookies.get(NINETAILED_ANONYMOUS_ID_COOKIE)?.value
35 events: [{ pageEvent }],
36 },
37 { ip: req.ip } // Pass original IP address
38 );
39
40 // If no profile ID cookie present, be sure to write it in a response
41 return apiResponse;
42}

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