Was this page helpful?

Experience SDK

Important: This Experience SDK is for Ninetailed use. If you are looking for the Experiences SDK for studio, see the Studio documentation

Table of contents

Overview

Ninetailed provides SDKs and plugins for modern web frameworks to enable you to harness the power of real-time personalization. All SDKs are written in Typescript to maximize your efficiency through code completion and compile-time analysis.

The Ninetailed SDKs provide abstracted means to communicate with the Ninetailed Experience API and the browser. It shortens integration times into JavaScript codebases. The SDKs handle:

  • creating page, track, and identify events and sending them to the Experience API
  • errors, retries and queuing
  • caching the profile client-side via localStorage.

Composed JavaScript SDKs

Ninetailed's lowest-level SDK is the Shared SDK, @ninetailed/experience.js-shared, whose primary function is to create an API Client that handles the details of working with the endpoints of the Experience API. It also exposes data objects, types, and methods applicable across development environments

Ninetailed's JavaScript SDK, @ninetailed/experience.js, is composed from the Shared SDK. The principal function of the JavaScript SDK is to create a Ninetailed instance that stores and updates a profile in response to events. The JavaScript and Shared SDK form the foundations on top of which all other Ninetailed JavaScript-based SDKs are built.

Supported frameworks

For declarative front-end implementations, Ninetailed provides SDKs for:

Additionally, Ninetailed provides a Node.js SDK for interacting with the Experience API in server or edge runtimes.

Choose an SDK

We strongly recommend using the SDK available for your framework if one is available for the fastest, most declarative implemenation.

For most Ninetailed users, installing the appropriate React-based SDK, sending page, track, and identify events to enhance user profiles, and using the <Experience> component to render personalization and experimentcontent client-side will be sufficient.

However, you may need to arbitrarily access profile data or exercise more control over how experiences are rendered and tracked.

These needs usually arise when:

  1. you need more control within a React-based web application, like if you're edge- or server-side rendering experiences and/or using React Server Components; or
  2. you're not implementing Ninetailed within a web-based React project (e.g., Vue/Nuxt, Svelte or React Native).

In such scenarios, the API Client created by the Shared SDK serves as the best starting place for upserting profiles in server and edge runtimes, while the JavaScript SDK is provides declarative tracking behaviour to use in client-side code.

For any front-end integration that is not based on Javascript, explore our Experience API for complete freedom on where to integrate.

Install the SDK

Dependency installation

npm install @ninetailed/experience.js-react
# OR
yarn add @ninetailed/experience.js-react
npm install @ninetailed/experience.js-next
# OR
yarn add @ninetailed/experience.js-next
npm install @ninetailed/experience.js-gatsby
# OR
yarn add @ninetailed/experience.js-gatsby
npm install @ninetailed/experience.js
# OR
yarn add @ninetailed/experience.js

Create a Ninetailed instance

The core responsibility of the Experience SDKs is to define a Ninetailed class instance based on your input configuration. The created instance is then responsible for:

  • keeping track of the current profile and providing a hook into when the profile changes
  • exposing declarative methods to interact with the Experience API
  • providing extensibility to the instance through plugins

When working with the React, Next.js or Gatsby SDK, a Ninetailed instance will be created and made available to your application globally using a React context provider.

Important: Your API Key can be found in the Ninetailed Dashboard.

Add the <NinetailedProvider> component to the top level of the application, so that the Ninetailed profile can be consumed from any child component.

import React from 'react';
import MyAppCode from '../myAppCode.jsx';
import { NinetailedProvider } from '@ninetailed/experience.js-react';

export const App = () => {
  return (
    <div id="my-app">
      <NinetailedProvider 
        // REQUIRED. An API key uniquely identifying your Ninetailed account.
        clientId="NINETAILED_API_KEY"

        // === ALL OF THE FOLLOWING PROPS ARE OPTIONAL ===
        // === DEFAULT VALUES ARE SHOWN ===

        // Your Ninetailed environment, typically either "main" or "development"
        environment="main"

        // Add any plugin instances
        plugins=[]

        // Specify an amount of time (ms) that an <Experience /> component must be present in the viewport to register a component view
        componentViewTrackingThreshold={2000}

        // Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
        requestTimeout={5000}

        // Specify a locale to localize profile location information
        locale="en-US"

        // Specify an alternative Experience API base URL
        url="https://experience.ninetailed.co"

        // Set to to true ONLY if using an unindexed CMS
        useSDKEvaluation=true
      >
        <MyAppCode />
      </NinetailedProvider>
    </div>
  );   
}

Add the <NinetailedProvider> component to the top level of the application, so that the Ninetailed profile can be consumed from any child component.

The Next.js <NinetailedProvider> also hooks into the Next's page router to automatically call ninetailed.page() on every route change. Do not additionally call this method on your own, otherwise you will duplicate events.

import { NinetailedProvider } from '@ninetailed/experience.js-next';

export const App = ({component, pageProps}) => {
  return (
    <div id="my-app">
      <NinetailedProvider 
        // REQUIRED. An API key uniquely identifying your Ninetailed account.
        clientId="NINETAILED_API_KEY"

        // === ALL OF THE FOLLOWING PROPS ARE OPTIONAL ===
        // === DEFAULT VALUES ARE SHOWN ===

        // Your Ninetailed environment, typically either "main" or "development"
        environment="main"

        // Add any plugin instances
        plugins=[]

        // Specify an amount of time (ms) that an <Experience /> component must be present in the viewport to register a component view
        componentViewTrackingThreshold={2000}

        // Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
        requestTimeout={5000}

        // Specify a locale to localize profile location information
        locale="en-US"

        // Specify an alternative Experience API base URL
        url="https://experience.ninetailed.co"

        // Set to to true ONLY if using an unindexed CMS
        useSDKEvaluation=true
      >
        <Component {...pageProps} />
      </NinetailedProvider>
    </div>
  );   
}
}

Add the plugin to your plugins array in your gatsby-config.js|ts file. By using the Gatsby JS plugin there's no need to configure the NinetailedProvider as described for the React and Next.js SDKs.

The plugin automatically calls ninetailed.page() on every route change. Do not additionally call this method on your own, otherwise you will duplicate events.

... // Your Other Gatsby configuration
plugins: [
  ... // Your existing Gatsby plugins
  {
    resolve: `@ninetailed/experience.js-gatsby`,
    options: {
      // REQUIRED. An API key uniquely identifying your Ninetailed account.
      clientId: "NINETAILED_API_KEY", 

      // === ALL OF FOLLOWING PROPERTIRES ARE OPTIONAL ===
      // === DEFAULT VALUES ARE SHOWN ===

      // Your Ninetailed environment, typically either "main" or "development"
      environment: "main",

      // Add any plugin instances
      ninetailedPlugins: [],

      // Specify an amount of time (ms) that a component must be present in the viewport to register a component view
      componentViewTrackingThreshold: 2000,

      // Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
      requestTimeout: 5000,

      // Specify a locale to localize profile location information
      locale: "en-US",

      // Specify an alternative Experience API base URL
      url: "https://experience.ninetailed.co",

      // Set to to true ONLY if using an unindexed CMS
      useSDKEvaluation: true
    }
  }
]
import { Ninetailed } from '@ninetailed/experience.js';

export const ninetailed = new Ninetailed(
   { 
        // REQUIRED. An API key uniquely identifying your Ninetailed account.
        clientId: "NINETAILED_API_KEY", 
        // OPTIONAL. Your Ninetailed environment, typically either "main" or "development"
        environment: "main" // Default
    }, 
    // === THE FOLLOWING ARGUMENT AND ALL OF ITS PROPERTIRES ARE OPTIONAL ===
    // === DEFAULT VALUES ARE SHOWN ===
    { 
        // Add any plugin instances
        plugins: [],

        // Specify an amount of time (ms) that a component must be present in the viewport to register a component view
        componentViewTrackingThreshold: 2000,

        // Specify a maximum amount of time (ms) to wait for an Experience API response before falling back to baseline content
        requestTimeout: 5000,

        // Specify a locale to localize profile location information
        locale: "en-US",

        // Specify an alternative Experience API base URL
        url: "https://experience.ninetailed.co"

        // Set to to true ONLY if using an unindexed
        useSDKEvaluation: true
    }
);

Browser instance access

Installing the Experience SDK exposes several Ninetailed properties and methods on a window.ninetailed object to facilitate testing and debugging.

Properties and methods Description
page(), track(), and identify() The core tracking methods of Ninetailed.
debug(arg: boolean) Turn on debug mode, which will output additional information about your experiences assignement to the console.
plugins Access the methods of any plugins attached to the Ninetailed instance.
profile Output the current profile state.
reset() Discard the current profile.

For a full description of instance properties and methods available to your application, consult the Ninetailed Instance documentation.

Send events

Ninetailed profiles are created and updated by sending events about that profile to the Experience API. Rather than interacting with the Experience API endpoints directly, the principal way to send events is to use the methods made available from the Ninetailed JavaScript SDK.

A Ninetailed class instance created by any of Ninetailed's SDKs composed from the JavaScript SDK provides three methods for sending events: page, track and identify.

page

type Page = (properties?: Object) => Promise<void>;

A page event indicates a user has viewed the current page. The SDK provides a context object describing the parameters of page that has been viewed including the referrer, url, path, user-agent and other properties to be consumed by the API.

While most of the time you will call this page method with no arguments, you may optionally specify any properties you would like to associate with the page view event. This can be useful for when creating Audiences. For example, if the category to which viewed blog posts belong is not contained within the URL of the blog posts but you'd like to create an Audience of visitors who have viewed blog posts of a certain category a certain number of times, you can pass the category along in the argument:

page({'category': 'YOUR_BLOG_CATEGORY'})
NOTE: Our Next.js and Gatsby SDKs automatically call a page event on every route change. If your application uses the React SDK or JavaScript SDK, you should manually implement a page call on every route change.

track

type Track = (event: string, properties?: Object) => Promise<void>;

track events are used to log specific named actions a user takes, like signup or registered_for_newsletter. Events can be named anything you like.

Track events may also accept a properties argument detailing additional information. For example, you might include the SKU and quantity of items on an add_to_cart event.

track('add_to_cart', {sku: '9T41L', quantity: 1})

In addition to serving as an Audience rule, track events are used to indicate the conversion events you want to measure in Experience Insights.

identify

type Identify = (uid: string, traits?: Traits) => Promise<void>;

Identify events serve two purposes:

First, identify allows you to add custom information, called traits, to a profile. Traits can be any attribute of interest about a customer. Any valid JSON is a valid trait. You can store any information of interest on profiles that you may want to use to segment users. For example, you might store a user's responses from a sign up form, a list of products they recently visited, or data from an upstream source of your customer data like a CRM or CDP.

identify('', { favoriteColor: "red" })

Second, identify allows you to name or "alias" a profile such that it can be referenced by that same alias in the future. IDs stored within an analytics system, customer data platform, or e-commerce platform make ideal aliases.

identify('customer12345')

After aliasing a profile, you can reinstate the aliased profile on a different device or browser by calling identify again using the same supplied alias. This merges the latest activity of the current profile with the profile at that alias. For personalizations and experiments served to logged in users, you will likely want to call identify after each successful authentication.

The flexibility of identify is powerful but should be used with caution. In particular, you should consider what privacy legislation your application needs to abide by, and whether that affects what data should not be stored as traits. For example, you probably would not want to store contact information (email addresses, phone numbers, etc.) as traits or use them as aliases.

Access event methods

The useNinetailed hook provides the tracking functions page, track, and identify. These methods will call the Experience API using the configuration parameters supplied to a <NinetailedProvider>.

import React from 'react';

import { useNinetailed } from '@ninetailed/experience.js-react';
// or import { useNinetailed } from '@ninetailed/experience.js-next';
// or import { useNinetailed } from '@ninetailed/experience.js-gatsby';

export const myComponent = () => {
  const { page, track, identify } = useNinetailed();

  page();
  identify('anAlias', {someTrait: "value"});

  return (
    <button 
      type="button" 
      onClick={() => { track('add_to_cart'); }}
    >
      Add to Cart
    </button>
  );
}

The page, track, and identify methods are available directly on a Ninetailed class instance.

import { Ninetailed } from '@ninetailed/experience.js';

const ninetailed = new Ninetailed({ clientId: "NINETAILED_API_KEY"})

ninetailed.page();
ninetailed.track('myEvent')
ninetailed.identify('alias', {traitName: "traitValue"})

See the Ninetailed Instance documentation for a full list of configuration options and methods available from the JS SDK.

Utility libraries

Our Experience API utility libraries provide methods to map experience content to the format required by the <Experience> component exported by our React, Next.js, and Gatsby SDKs.

Use the Contentful Utility SDK if you are are retrieving content and experiences using Contentful's client libraries that interface with the Contentful REST APIs, including:

  • the Contentful Content Delivery API
  • the Contentful Content Preview API

For all other sources, including the Contentful GraphQL API or your own internal content APIs, middleware, use the JavaScript Utility SDK and map your experiences to the type required by the ExperienceMapper class methods.

JavaScript library usage

npm install @ninetailed/experience.js-utils
# OR
yarn add @ninetailed/experience.js-utils

You must map your fetched CMS Experience entries to a particular shape prior to transforming them with the ExperienceMapper methods. The following examples show the format required in a .map step prior to calling .filter to remove ill-formatted entries.

import { ExperienceMapper } from '@ninetailed/experience.js-utils';

const mappedExperiences = (myEntry.nt_experiences || [])
  .map((experience) => {
    return {
      id: experience.id,
      name: experience.name
      type: experience.nt_type as 'nt_personalization' | 'nt_experiment'
      config: experience.nt_config,
      audience: {
        id: experience.nt_audience.nt_audience_id
        // If mapping for the Preview Plugin, this displays audience names
        name: experience.nt_audience.nt_name
      },
      variants: experience.variants.map((variant) => {
        return {
          id: variant.id, // Required
          // Map any other fields required by your components
          ...variant,
          someComponentProp: variant.foo
        }
      })
    }
  })
  .filter((experience) => ExperienceMapper.isExperienceEntry(experience))
  .map((experience) => ExperienceMapper.mapExperience(experience));

Your exact query and mapping will vary depending on both your content model and the props required by the component you use to render your content. This example assumes a content model using a content type of page that contains a field called sections that can reference entries of type hero.

It also shows using a lightweight GraphQL client library graphql-request to make the API request, but any GraphQL client is suitable.

import { request, gql } from "graphql-request";
import { filterAndMapExperiences, mapAudiences } from "../lib/helpers";

const CONTENTFUL_HERO_QUERY = gql`
  fragment SysId on Entry {
    sys {
      id
    }
  }

  fragment HeroEntry on Hero {
    ...SysId
    internalName
  }

  fragment NtExperienceFields on NtExperience {
    ...SysId
    ntExperienceId
    ntName
    ntType
    ntConfig
    ntAudience {
      ntAudienceId
    }
  }

  fragment NinetailedHero on Hero {
    ...HeroEntry
    ntExperiencesCollection(limit: 10) {
      items {
        ...NtExperienceFields
        ntVariantsCollection(limit: 10) {
          items {
            ...HeroEntry
          }
        }
      }
    }
  }


  query NinetailedHeroQuery($heroEntryid: String!) {
    page(id: $heroEntryid) {
      ...SysId
      sectionsCollection(limit: 10) {
        items {
          ...NinetailedHero
        }
      }
    }
  }
`;


export async function getHeroData(heroId) {
  const data = await request(
    `https://graphql.contentful.com/content/v1/spaces/${process.env.CTFL_SPACE_ID}`,
    CONTENTFUL_HERO_QUERY,
    heroId,
    {
      Authorization: `Bearer ${process.env.CTFL_API_KEY}`,
      "Content-Type": "application/json",
    }
  );
  return data;
}
import { ExperienceMapper } from '@ninetailed/experience.js-utils';
import { getHeroData } from 'api/yourDataFetcher';

const hero = await getHeroData('aHeroEntryId')

const mappedExperiences = (hero.ntExperiencesCollection?.items || [])
  .map((experience) => {
    return {
      id: experience.ntExperienceId,
      name: experience.ntName,
      type: experience.ntType,
      config: experience.ntConfig,
      // This syntax accounts for the possibility of an audience not being set on an experiment
      ...(experience.ntAudience
        ? { 
            audience: {
              id: experience.ntAudience.ntAudienceId
              // If mapping for the Preview Plugin, this displays audience names
              name: experience.ntAudience.ntName
            },
          }
        } : {})
      variants: experience.ntVariantsCollection.items.map((variant) => {
        return {
          id: variant.sys.id, // Required
          // Map any other fields required by your rendering component
          ...variant
        }
      })
    }
  })
  .filter((experience) => ExperienceMapper.isExperienceEntry(experience))
  .map((experience) => ExperienceMapper.mapExperience(experience));

Notice the use of fragments to capture the sys.id, since this is required on the Ninetailed Experience (NtExperience) entry as well as all variants referenced by the entry. Additionally, note the use of a fragment to isolate the fields of the Experience entry so that the base HeroEntry fragment can be used to query both the baseline and the variant content without introducing a circular reference.

Contentful library usage

npm install @ninetailed/experience.js-utils-contentful
# OR
yarn add @ninetailed/experience.js-utils-contentful
import { ExperienceMapper } from '@ninetailed/experience.js-utils-contentful'
import { createClient } from 'contentful';

const client = createClient({
  accessToken: 'youtAccessToken',
  space: 'yourSpaceId'
})

// Specify what entries with Ninetailed Experience references to get from Contentful
const query = {...}

const rawEntries = await client.getEntries(query);
// Extract one entry, as an example
const [yourEntry] = rawEntries.items

// Filter and map with ExperienceMapper methods
const experiences = (yourEntry.fields.nt_experiences || [])
    .filter(ExperienceMapper.isExperienceEntry)
    .map(ExperienceMapper.mapExperience)

See also our Contentful + Next.js example project for context.

ExperienceMapper class methods

isExperienceEntry

Determines if a provided entry is of valid type to be consumed by mapExperience. Use with .filter to remove any invalidly typed experiences.

mapExperience

Transform an experience to the type required by the <Experience> component.

isExperimentEntry

Determines if a provided entry is of valid type to be consumed by mapExperiment. Use with .filter to remove any invalidly typed experiments.

mapExperiment

Transform an experiment to the type required by the React and Next.js <NinetailedProvider> experiments prop.

mapCustomExperience

This class method is for the Contentful library only. If you need to modify how the variants referenced by an experience entry retrieved from Contentful are mapped, use this method to pass a custom variant mapping function. Example usage:

const experiences = myExperience.fields.nt_experiences
  .filter(ExperienceMapper.isExperienceEntry)
  .map((experience) => {
    ExperienceMapper.mapCustomExperience(experience, (variant) => {
      id: variant.sys.id // required
      // Add any data required by your `component` prop on the <Experience> component
      ...variant.fields,
      someComponentProp: variant.foo
    });
  })

mapCustomExperienceAsync

This class method is for the Contentful library only, SDK >= 7.7.x. Similar to mapCustomExperience, but allows asynchronous operations to be executed in the variant mapping step.

const experiences = myExperience.fields.nt_experiences
  .filter(ExperienceMapper.isExperienceEntry)
  .map((experience) => {
    ExperienceMapper.mapCustomExperienceAsync(experience, async (variant) => {
      await new Promise ((resolve) => setTimeout(resolve, 1000)); // Simulated delay supported by async handler
      return {
        id: variant.sys.id // required
        // Add any data required by your `component` prop on the <Experience> component
        ...variant.fields,
        someComponentProp: variant.foo
      }
    });
  })

mapBaselineWithExperiences

This class method is for the Contentful library only. Supply an object representing a baseline entry and it's attached experiences and return an array of filtered and mapped experiences.

Render Experiences

The <Experience> component

The Ninetailed React-based SDKs provide an <Experience> component to wrap your existing React components you use to render your content. This is the most declarative way to render Ninetailed personalization and experiment content, and therefore the methodology that most Ninetailed users should adopt when able.

The <Experience> component functions wraps your existing React component. It automatically detects the properties needed from the wrapped component. The experiences prop accepts Ninetailed Experience content that has been appropriately transformed by the ExperienceMapper available from our Utility libraries.

<Experience> component props

PropDescription
{...baseline}[Required] Any and all props that the function passed as the component prop needs to receive to render the baseline variant entry. This will depend entirely on the structure of your existing React component(s).
id[Required] The CMS entry ID of the baseline
component[Required] The React component that your baseline and variants will use to render. This can either be regular React component or a component that opts into React's forwardRef.
experiences[Required] An array of experience CMS entries mapped using the ExperienceMapper methods available from our utility libraries
passthroughProps[Optional] An object containing key-value pairs of props that should be sent to the component irrespective of which experience variant is selected. Props supplied here will overwrite those of the selected variant, so this is designed for non-content props like state or refs.
loadingComponent[Optional] A custom component to show prior to the <Experience> component selecting a variant. This defaults to a transparent version of your component using the baseline props.

Example use

These examples show working CMS data, our Utility Libraries, and the <Experience> component together in demonstrative examples. Your implementation will vary according to your existing React components and your data source. Consult the Utility libraries documentation to know what data to fetch from your content source and how to transform the returned Experience entries to the format required by the <Experience> component.

These examples show fetching CMS data from within potentially deeply nested React components. In practice, you will likely fetch that data from higher within your rendering tree and pass it to components, especially when statically pre-rendering. However, the mapping exercises and use of the <Experience> component demonstrated remain the same no matter what rendering strategy you adopt.

// or '@ninetailed/experience.js-next', '@ninetailed/experience.js-gatsby'
import { Experience } from '@ninetailed/experience.js-react';
import { ExperienceMapper } from '@ninetailed/experience.js-utils'

// This function is assumed to return a single entry and all its supporting data, including referenced content, in their entirety
import { getCmsEntry } from '../api/yourEntryGetter'
import { YourComponent } from './YourComponent'

export const YourExperience = (cmsBaselineEntry) => {
  const baselineEntry = getCmsEntry(cmsBaselineEntry);
  const experiences = baselineEntry['nt_experiences']

  const mappedExperiences = (experiences || [])
  .map((experience) => {
    return {
      id: experience.id,
      name: experience.name
      type: experience.nt_type as 'nt_personalization' | 'nt_experiment'
      config: experience.nt_config,
      audience: {
        id: experience.nt_audience.nt_audience_id
      },
      variants: experience.variants.map((variant) => {
        return {
          id: variant.id, // Required
          // Map any other data from the variant required by your component
          ...variant,
          someComponentProp: variant.foo
        }
      })
    }
  })
  .filter((experience) => ExperienceMapper.isExperienceEntry(experience))
  .map((experience) => ExperienceMapper.mapExperience(experience));

  return (
    <Experience
      {...baselineEntry} // Any props your `component` above needs
      id={entry.id} // Required. The id of the BASELINE entry
      component={YourComponent} // Required. What to use to render the selected variant
      experiences={mappedExperiences} // Array of mapped experiences
  />);
};
// or '@ninetailed/experience.js-next', '@ninetailed/experience.js-gatsby'
import { Experience } from '@ninetailed/experience.js-react';

// For use with Contentful REST APIs only
import { ExperienceMapper } from '@ninetailed/experience.js-utils-contentful'

// This function is assumed to return a single entry and all its nested references from the REST Contentful CDA in their entirety
import { getContentfulEntry } from '../api/yourEntryGetter'
import { YourComponent } from './YourComponent'

export const YourExperience = (cmsBaselineEntry) => {
  const baselineEntry = getContentfulEntry(cmsBaselineEntry);
  const experiences = baselineEntry.fields.nt_experiences;
  const mappedExperiences = experiences
    .filter((experience) => ExperienceMapper.isExperienceEntry(experience))
    .map((experience) => ExperienceMapper.mapExperience(experience))

  return (
    <Experience
      {...baselineEntry} // Any props your `component` above needs
      id={baselineEntry.sys.id} // Required. The sys.id of the BASELINE entry
      component={YourComponent} // Required. What to use to render the selected variant
      experiences={mappedExperiences} // Array of mapped experiences
  />);
};

Inline Personalization with merge tags

Ninetailed allows you to embed content placeholders into Rich Text Fields that can then be rendered client-side using information from the current visitor's profile. These dynamic placeholder entries are called Merge Tags, which can then be used as inline entries within a rich text field of your CMS entries.

The React-based SDKs provide a corresponding <MergeTag /> component that allow you to declaratively render the inlined Merge Tag entries.

While rendering Merge Tag entries embedded within Rich Text Fields is the most common use for merge tags, you simply pass the property accessor (using dot notation) of any Ninetailed profile property as the id of the MergeTag component.

import React from 'react';
import { INLINES } from '@contentful/rich-text-types';
import { documentToReactComponents} from '@contentful/rich-text-react-renderer';
// or `@ninetailed/experience.js-next', @ninetailed/experience.js-gatsby'
import { MergeTag } from '@ninetailed/experience.js-react';

export const renderRichText = (richTextDocument) => {
  return documentToReactComponents(richTextDocument, {
    renderNode: {
      [INLINES.EMBEDDED_ENTRY]: (node) => {
        if (node.data.target.sys.contentType.sys.id === 'nt_mergetag')) {
          return (
            <MergeTag 
              id={node.data.target.fields.nt_mergetag_id}
              fallback={node.data.target.fields.nt_fallback}
            />
          );
        }
      }
    },
  });
};
import React from 'react';
// or `@ninetailed/experience.js-next', @ninetailed/experience.js-gatsby'
import { MergeTag } from '@ninetailed/experience.js-react';

const Greeting = () => {
  return (
  <> 
    <p>Welcome back, <MergeTag id="traits.firstName" fallback="you" />
    <p>How is <MergeTag id="location.city" fallback="your city" /> this time of year?</p>
  </>
};

Tracking impressions of Experiences

The <Experience> component needs to track when the markup it renders is present within the visitor's viewport. This is the criteria used to fire impression events to any connected Ninetailed plugins.

Unless the component prop being passed to <Experience> is defined as a React forwardRef, the <Experience> component will insert an empty non-displaying <div> of class nt-cmp-marker immediately prior to the rendered component. It is this inserted element's intersection with the viewport that then tracked.

// Returned markup from the <Experience> component when passing a regular component
<div className="nt-cmp-marker" style="display: none !important">
<!-- Your rendered component content -->
<div>
    <!-- ... -->
</div>

Under the hood, the tracking component uses React's useRef to store the DOM node to track. See the React forwardRef documentation for more details.

Because of the use of useRef, it is important that the components you pass have some consistent parent element between re-renders. Conditionally rendering top-level elements in a component may cause tracking to become decoupled.

const SomeAsyncConditionalComponent = () => {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // ... Do something like get async data
    setLoading(false);
  }, []);

  // This component conditionally renders a top-level element, so tracking might be lost
  return loading ? (
    <div>Some loading markup</div>
  ) : (
    <div>Some markup after loading</div>
  );
};

const YourExperience = (cmsBaselineEntry) => {
  // ... Filter and map as above

  return (
    <Experience
      id={entry.id}
      component={SomeAsyncConditionalComponent}
      {...baselineEntry}
      experiences={mappedExperiences}
  />);
};
const SomeAsyncConditionalComponent = () => {
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // ... Do something like get async data
    setLoading(false);
  }, []);

// Wrapping an element around the conditional rendering to preserve tracking
return (
    <div> 
      (loading ? (<div>Some loading markup</div>) : (
      <div>Some markup after loading</div>) )
    </div>
  );
};

const YourExperience = (cmsBaselineEntry) => {
  // ... Filter and map as above

  return (
    <Experience
      id={entry.id}
      component={SomeAsyncConditionalComponent}
      {...baselineEntry}
      experiences={mappedExperiences}
  />);
};