Ninetailed
Ask or search…
K

Utility Libraries

More utilities to integrate Ninetailed within your current tech stack.
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 from Contentful's RESTful content APIs, including:
  • the Contentful Content Delivery API
  • the Contentful Content Preview API
For all other sources, including:
  • the Contentful GraphQL API
  • the Contentstack Content Delivery API
  • your own internal content APIs, middleware, etc.
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.
Generic
Contentful GraphQL API
Contentstack Delivery API
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
},
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.
api/yourDataFetcher.js
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
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.sys.id,
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
},
}
} : {})
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.
Your exact query and mapping will vary depending on:
  • your content model,
  • the props required by the component you use to render your content, and
  • whether the Experiences you are mapping are attached to a modular block or a standalone entry
Note that because Ninetailed extends your content model with additional references, you'll need to fetch additional data from the Contentstack Delivery API to power this mapping.
We highly recommend browsing the Contentstack + Next.js example project to provide context for these code samples. These code examples make use of some utility functions to determine whether the content being mapped is a modular block and if so, how to retrieve its uid. You can also explore how to fetch the additional Experience content in the example project.

Standalone Entry Experience

import {
ExperienceLike,
ExperienceMapper,
} from '@ninetailed/experience.js-utils';
import { NtExperience } from '@/types/contentstack';
import { getBlockId } from './modularblocks';
interface NtExperienceCsEntry extends NtExperience {
uid: string;
}
export function parseExperiences(entry: any) {
return entry?.nt_experiences
? entry.nt_experiences
.map((experience: NtExperienceCsEntry) => {
return {
name: experience.nt_name,
type: experience.nt_type,
config: experience.nt_config,
...(experience.nt_audience?.length
? {
audience: {
id: experience.nt_audience[0].nt_audience_id,
name: experience.nt_audience[0].nt_name,
},
}
: {}),
id: experience.uid,
variants: experience.nt_variants?.map((variant: any) => {
return {
id: variant.uid,
...variant,
};
}),
};
})
.filter((experience: ExperienceLike) =>
ExperienceMapper.isExperienceEntry(experience)
)
.map((experience: ExperienceLike) =>
ExperienceMapper.mapExperience(experience)
)
: [];
}

Modular Block Experiences

Modular blocks require somewhat more complex mapping, because all experiences for all modular blocks on a page are stored in a top-level field of an entry. This mapping function demonstrates narrowing down this field to only experiences relevant to a single modular block, mapping its associated variants, and finally mapping to the format required by the <Experience> component.
import {
ExperienceLike,
ExperienceMapper,
} from '@ninetailed/experience.js-utils';
import { NtExperience } from '@/types/contentstack';
import { getBlockId } from './modularblocks';
export function parseModularBlockExperiences(
modularBlockExperiences: any, // fetched from `nt_modular_blocks_experiences` field
blockId: string // the uid of the baseline modular block
) {
if (modularBlockExperiences && modularBlockExperiences.length) {
const scopedExperiences = modularBlockExperiences.filter(
(experience: any) => {
return experience.nt_experience_block.nt_baseline.uid === blockId;
}
);
if (!scopedExperiences.length) {
return [];
}
const mappedVariants = (
(scopedExperiences.length &&
scopedExperiences.map((experience: any) => {
const variants = experience.nt_experience_block.nt_variants;
return variants?.map((variant: any) => {
return {
...variant,
id: getBlockId(variant),
};
});
})) ||
[]
).flat();
return scopedExperiences
.map((scopedExperience: any) => {
const experience =
scopedExperience.nt_experience_block.nt_experience[0];
return {
name: experience.nt_name,
type: experience.nt_type,
config: {
...experience.nt_config,
components: experience.nt_config?.components?.map(
(component: { variants: any[]; baseline: { blockId: any } }) => {
return {
variants: component.variants?.map((variant) => {
return {
...variant,
id: variant.blockId,
};
}),
baseline: {
...component.baseline,
id: component.baseline.blockId,
},
};
}
),
},
...(experience.nt_audience.length
? {
audience: {
id: experience.nt_audience[0].nt_audience_id,
name: experience.nt_audience[0].nt_name,
},
}
: {}),
id: experience.uid,
variants: mappedVariants,
};
})
.filter((experience: any) =>
ExperienceMapper.isExperienceEntry(experience)
)
.map((experience: any) => ExperienceMapper.mapExperience(experience));
}
return [];
}

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

[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
});
})

mapBaselineWithExperiences

[Contentful Library only] Supply an object representing a baseline entry and it's attached experiences and return an array of filtered and mapped experiences.