Skill 09 · Contentful Personalization
Subchapter 9.24
references/sdk-legacy-guide.mdMarkdown29 KBView on GitHub
Complete API reference for diagnosing, repairing, or extending deployments that already use the Ninetailed legacy SDK packages. Do not use this reference to start a new integration.
| Package | Purpose |
|---|---|
@ninetailed/experience.js | Core SDK (browser) |
@ninetailed/experience.js-react | React bindings (Provider, hooks, components) |
@ninetailed/experience.js-next | Next.js integration (auto page tracking, re-exports React + SSR) |
@ninetailed/experience.js-shared | Shared types, NinetailedApiClient, event builders |
@ninetailed/experience.js-utils-contentful | ExperienceMapper, AudienceMapper for Contentful REST API |
@ninetailed/experience.js-node | Server-side Node.js SDK |
@ninetailed/experience.js-plugin-insights | Component view/click/hover tracking via Beacon API |
@ninetailed/experience.js-plugin-preview | Preview editor UI widget |
@ninetailed/experience.js-plugin-ssr | SSR/ESR support (cookie-based anonymous ID persistence) |
@ninetailed/experience.js-plugin-privacy | Consent management / GDPR event filtering |
@ninetailed/experience.js-plugin-segment | Segment CDP integration |
@ninetailed/experience.js-plugin-google-tagmanager | GTM data layer integration |
@ninetailed/experience.js-plugin-google-analytics | Google Analytics (gtag) integration |
@ninetailed/experience.js-plugin-contentsquare | Contentsquare integration |
import { Ninetailed } from '@ninetailed/experience.js';
const ninetailed = new Ninetailed(
// First argument: NinetailedApiClientOptions OR NinetailedApiClient instance
{
clientId: string, // Required: API key
environment?: string, // Default: 'main'
preview?: boolean, // Enable preview mode
},
// Second argument: Options
{
url?: string, // Override API base URL
locale?: Locale, // Language locale string
plugins?: (NinetailedPlugin | NinetailedPlugin[])[], // Plugins (nested arrays flattened)
requestTimeout?: number, // Timeout for API requests
onLog?: OnLogHandler, // Custom log handler
onError?: OnErrorHandler, // Custom error handler
componentViewTrackingThreshold?: number, // Default: 2000 (ms)
componentHoverTrackingThreshold?: number, // Default: 2000 (ms)
onInitProfileId?: OnInitProfileId, // Callback when profile ID is first initialized
buildClientContext?: () => NinetailedRequestContext, // Custom context builder
storageImpl?: Storage, // Custom storage ({ getItem, setItem, removeItem })
}
);Sends a pageview event. Waits for SDK initialization, then fires through the analytics instance and flushes.
await ninetailed.page({ url: '/some-page', title: 'My Page' });Sends a custom tracking event.
await ninetailed.track('button_click', { buttonId: 'cta-hero' });Identifies a user by ID and optional traits. If uid is empty string, applies traits without triggering a merge.
await ninetailed.identify('user-123', { plan: 'enterprise', role: 'admin' });Sends multiple events at once.
await ninetailed.batch([
{ type: 'page', properties: {} },
{ type: 'track', event: 'signup', properties: {} },
]);Resets the SDK state (clears profile, cookies, etc.).
Enables or disables debug mode.
Subscribes to profile state changes. The callback is called immediately with the current state, then on every subsequent change. Returns an unsubscribe function.
const unsubscribe = ninetailed.onProfileChange((profileState) => {
if (profileState.status === 'success') {
console.log('Profile:', profileState.profile);
console.log('Experiences:', profileState.experiences);
}
});Core method for experience resolution. Given a baseline and array of experiences, subscribes to profile changes and calls cb with the resolved variant. This is what powers the <Experience> component.
The callback receives:
status: 'loading' | 'success' | 'error'loading: booleanhasVariants: booleanbaseline, experience, variant, variantIndexaudience: { id: string } | nullisPersonalized: booleanprofile: Profile | nullerror: Error | nullRegisters an element for intersection observation (view tracking), click tracking, and hover tracking.
// ObserveOptions:
{ delay?: number, trackClicks?: boolean, trackHovers?: boolean }Stops observing an element.
type ProfileState =
| { status: 'loading'; profile: null; experiences: null; changes: null; error: null; from: 'api' | 'hydrated' }
| {
status: 'success';
profile: Profile;
experiences: SelectedVariantInfo[];
changes: Change[];
error: null;
from: 'api' | 'hydrated';
}
| {
status: 'error';
profile: Profile | null;
experiences: SelectedVariantInfo[] | null;
changes: Change[] | null;
error: Error;
from: 'api' | 'hydrated';
};The SDK attaches to window.ninetailed with:
page(), track(), identify(), reset(), debug() – non-async wrappersprofile – current profile objectexperiences – current experience selectionsPackage: @ninetailed/experience.js-react
Creates a Ninetailed instance (or accepts a pre-created one) and puts it in React context.
// Instantiation mode (creates Ninetailed internally)
type NinetailedProviderInstantiationProps = {
clientId: string;
environment?: string;
preview?: boolean;
url?: string;
plugins?: (NinetailedPlugin | NinetailedPlugin[])[];
locale?: Locale;
requestTimeout?: number;
onLog?: OnLogHandler;
onError?: OnErrorHandler;
componentViewTrackingThreshold?: number;
componentHoverTrackingThreshold?: number;
buildClientContext?: () => NinetailedRequestContext;
onInitProfileId?: OnInitProfileId;
storageImpl?: Storage;
};
// Instance mode (pass a pre-created Ninetailed)
type NinetailedProviderProps = NinetailedProviderInstantiationProps | { ninetailed: Ninetailed };Returns the NinetailedInstance from context. Throws if used outside NinetailedProvider. Provides access to all core SDK methods.
const { track, identify, page, reset, debug, profileState } = useNinetailed();Returns the current profile state with the experiences property stripped (to prevent unnecessary re-renders).
type UseProfileHookResult = Omit<ProfileState, 'experiences'> & {
loading: boolean; // true when status === 'loading'
};const { profile, loading, status, error } = useProfile();
if (loading) return <Spinner />;
console.log(profile.traits, profile.location, profile.audiences);The hook that powers the <Experience> component.
const { status, hasVariants, experience, variant, variantIndex, audience, isPersonalized, profile, error } =
useExperience({ baseline, experiences });Return type is a discriminated union by status: 'loading' | 'success' | 'error'.
Accesses a Ninetailed variable (feature flag) with built-in auto-tracking.
type FlagResult<T> =
| { status: 'loading'; value: T; error: null }
| { status: 'success'; value: T; error: null }
| { status: 'error'; value: T; error: Error };
type UseFlagOptions = {
shouldAutoTrack?: boolean | (() => boolean); // default: true
};const { status, value, error } = useFlag('banner-text', 'default text');
const { value: config } = useFlag<{ padding: string; color: string }>('hero-config', {
padding: '10px',
color: 'blue',
});Same as useFlag but returns a tuple with the flag result and a manual track function. Useful when you want to control exactly when the impression is tracked.
const [flag, track] = useFlagWithManualTracking<{
padding: string;
color: string;
}>('testing-component-tracking', { padding: '10px', color: 'blue' });
const handleClick = () => {
track();
// ... user interaction
};Selects a variant for simple (non-experience-based) personalization.
const { loading, variant, isPersonalized, audience } = usePersonalize(
baseline,
variants,
{ holdout: -1 }, // holdout percentage (-1 = disabled)
);The primary personalization component. Resolves which variant to show based on experience configuration and the current profile.
type ExperienceProps<P, PassThroughProps, Variant> = {
id: string; // Required: baseline entry ID
experiences: ExperienceConfiguration<Variant>[]; // Mapped experiences
component: ComponentType<P>; // The component to render
loadingComponent?: ExperienceLoadingComponent; // Custom loading component
passthroughProps?: PassThroughProps; // Props passed regardless of variant
trackClicks?: boolean; // Enable click tracking
trackHovers?: boolean; // Enable hover tracking
// ...all other baseline props spread
};Behavior:
LoadingComponent (default hides baseline with visibility: hidden)hidden: true, renders only a tracking markerninetailed prop: { isPersonalized, audience: { id } }<Experience
{...entry.fields}
id={entry.sys.id}
component={ComponentRenderer}
experiences={mappedExperiences}
trackClicks
trackHovers
loadingComponent={ESRLoadingComponent}
/>Loading component for Edge-Side Rendering. Reads the experienceVariantsMap from ESRContext and renders the pre-resolved variant immediately (no flicker).
type ESRProviderProps = {
experienceVariantsMap: Record<string, number>; // experienceId -> variantIndex
};A simplified personalization component (legacy data model).
type PersonalizeProps<P> = P & {
id: string;
variants?: Variant<P>[];
component: PersonalizedComponent<P>;
loadingComponent?: React.ComponentType;
holdout?: number; // default: -1
};Renders a profile trait value inline.
type MergeTagProps = {
id: string; // Trait path (e.g. 'traits_company' or 'location_city')
fallback?: string; // Fallback value if trait not found
};The id uses underscores that are converted to nested dot paths. For example traits_company_name tries selectors like traits.company_name, traits.company.name, etc.
<MergeTag id="traits_firstName" fallback="there" />
// Renders: profile.traits.firstName or "there"Wraps <Experience> with an empty experiences array. Useful for tracking views/clicks on entries without any personalization.
<EntryAnalytics
{...entry}
id={entry.id}
component={MyComponent}
trackClicks
/>type Variant<P = unknown> = P & {
id: string;
audience: { id: string };
};Package: @ninetailed/experience.js-next
Re-exports everything from @ninetailed/experience.js-react and @ninetailed/experience.js-plugin-ssr, then adds Next.js-specific features.
Wraps the React NinetailedProvider and adds a <Tracker /> component for automatic page tracking on route changes.
type NextNinetailedProviderProps = NinetailedProviderProps & {
onRouteChange?: OnRouteChange;
};
type OnRouteChange = (routeInfo: { isInitialRoute: boolean }, ninetailed: NinetailedInstance) => void;If onRouteChange is provided, it replaces the default ninetailed.page() call on route changes.
Listens to Next.js router events (routeChangeComplete) and automatically calls ninetailed.page() on each page navigation. Deduplicates calls. Fires on initial mount and on each subsequent route change.
Decodes a comma-separated string of experienceId=variantIndex pairs. Used in ESR patterns where the edge worker encodes variant selections in the URL.
// Input: "expId1=1,expId2=2"
// Output: { expId1: 1, expId2: 2 }
decodeExperienceVariantsMap('expId1=1,expId2=2');Package: @ninetailed/experience.js-shared
HTTP client for the Ninetailed Experience API.
const apiClient = new NinetailedApiClient({
clientId: string,
environment?: string, // Default: 'main'
url?: string, // Override base URL
fetchImpl?: FetchImpl, // Custom fetch for non-browser environments
});| Method | Description |
|---|---|
createProfile({ events }, options?) | Creates a new profile |
updateProfile({ profileId, events }, options?) | Updates an existing profile |
upsertProfile({ profileId?, events }, options?) | Create or update based on whether ID is present |
getProfile(id, options?) | Retrieve a profile by ID |
upsertManyProfiles({ events }, options?) | Batch upserts (each event needs anonymousId) |
All return Promise<ProfileWithSelectedVariants>.
type RequestOptions = {
timeout?: number; // Default: 3000ms
preflight?: boolean; // ESR/SSR mode: evaluate but don't persist
locale?: string;
ip?: string; // Override IP for server-side calls
plainText?: boolean; // Default: true. Avoids CORS preflight
retries?: number; // Default: 1. Only retries 503s
minRetryTimeout?: number; // Default: 0ms
enabledFeatures?: Feature[]; // 'ip-enrichment' | 'location'
};import { buildPageEvent, buildTrackEvent, buildIdentifyEvent } from '@ninetailed/experience.js-shared';
buildPageEvent({ messageId, timestamp, ctx, location?, properties });
buildTrackEvent({ messageId, timestamp, ctx, location?, event, properties });
buildIdentifyEvent({ messageId, timestamp, ctx, location?, userId, traits });The ctx parameter:
{
url: string;
referrer: string;
locale: string;
userAgent: string;
document?: { title: string };
}type Profile = {
id: string;
stableId: string;
random: number; // 0-1, used for traffic allocation
audiences: string[]; // Array of matched audience IDs
traits: Traits; // JSON object of user traits
location: GeoLocation;
session: SessionStatistics;
};type GeoLocation = {
coordinates?: { latitude: number; longitude: number };
city?: string;
postalCode?: string;
region?: string;
regionCode?: string;
country?: string;
countryCode?: Alpha2Code;
continent?: string;
timezone?: string;
};type ExperienceConfiguration<Variant extends Reference = Reference> = {
id: string;
type: 'nt_personalization' | 'nt_experiment';
name?: string;
description?: string;
audience?: { id: string; name?: string; description?: string };
trafficAllocation: number;
distribution: Distribution[];
sticky?: boolean;
components: (EntryReplacement<Variant> | InlineVariable)[];
};type SelectedVariantInfo = {
experienceId: string;
variantIndex: number;
variants: Record<string, string>;
sticky: boolean;
};type Change = {
type: 'Variable';
key: string;
value: string | boolean | number | JsonObject;
meta: { experienceId: string; variantIndex: number };
};Package: @ninetailed/experience.js-utils-contentful
For use with the Contentful REST APIs (Content Delivery API and Content Preview API). For GraphQL, use @ninetailed/experience.js-utils instead.
Type guard that validates whether a Contentful entry is a valid experience entry using Zod schema validation. Use with .filter().
Maps a Contentful experience entry to SDK format. Each variant gets { ...variant, id: variant.sys.id }.
Like mapExperience but accepts a custom variant mapping function.
ExperienceMapper.mapCustomExperience(ctfExperience, (variant) => ({
id: variant.sys.id,
...variant.fields,
}));Async version. Available in SDK >= 7.7.x.
Type guard for experiment entries specifically.
Maps an experiment entry. Variants are mapped to empty { id: '' }.
Convenience method that takes an entry with nt_experiences field, filters valid experiences, and maps them all.
const experiences = ExperienceMapper.mapBaselineWithExperiences(heroEntry);Type guard validating a Contentful entry as a valid audience entry.
Maps to { id, name, description }.
import { ExperienceMapper } from '@ninetailed/experience.js-utils-contentful';
const experiences = (entry.fields.nt_experiences || [])
.filter(ExperienceMapper.isExperienceEntry)
.map(ExperienceMapper.mapExperience);Package: @ninetailed/experience.js-plugin-insights
Tracks component views, clicks, and hovers. Sends batched events to the Insights API via Beacon API on page hide.
new NinetailedInsightsPlugin({ url?: string })Package: @ninetailed/experience.js-plugin-preview
Renders a preview widget UI for content editors to view/toggle audiences and force specific experience variants.
new NinetailedPreviewPlugin({
experiences: ExperienceConfiguration[], // All available experiences
audiences: ExposedAudienceDefinition[], // All available audiences
onOpenExperienceEditor?: (experience) => void, // Callback to open experience editor
onOpenAudienceEditor?: (audience) => void, // Callback to open audience editor
url?: string, // Preview bridge URL override
nonce?: string, // CSP nonce for script/style injection
ui?: { opener: { hide: boolean } }, // Widget UI options
})Key methods exposed via window.ninetailed.plugins.preview:
open(), close(), toggle() – widget visibilityactivateAudience(id), deactivateAudience(id), resetAudience(id)setExperienceVariant({ experienceId, variantIndex })resetExperience(experienceId)reset() – full SDK resetWhen to use: Include during development and preview modes. Pass all experiences and audiences. Does NOT auto-disable in production – conditionally instantiate.
const preview = process.env.NODE_ENV !== 'production';
plugins={[
...(preview ? [new NinetailedPreviewPlugin({ ... })] : []),
]}Package: @ninetailed/experience.js-plugin-ssr
Persists the Ninetailed anonymous ID in a cookie so that server-side/edge rendering can read the same profile ID.
new NinetailedSsrPlugin({
cookie?: {
domain?: string, // Cookie domain
expires?: number, // Days until expiry (default: 365)
}
})Behavior:
initialize: reads cookie value and sets it as the analytics anonymous IDPROFILE_CHANGE: writes the profile ID to the cookiePROFILE_RESET: removes the cookieWhen to use: Required for ESR patterns and any SSR scenario where the profile ID needs to persist across server and client.
Package: @ninetailed/experience.js-plugin-privacy
Manages consent and controls which events/properties are sent.
new NinetailedPrivacyPlugin(
config?: Partial<PrivacyConfig>, // Config when consent NOT given
acceptedConsentConfig?: Partial<PrivacyConfig> // Config when consent IS given (SDK >= 7.7)
)type PrivacyConfig = {
allowedEvents: EventType[]; // Default no-consent: ['page']
allowedPageEventProperties: string[]; // Default: ['*']
allowedTrackEvents: string[]; // Default: []
allowedTrackEventProperties: string[]; // Default: []
allowedTraits: string[]; // Default: []
blockProfileMerging: boolean; // Default: true
enabledFeatures: Feature[]; // Default: []
};Consent is managed via:
window.ninetailed.consent(true); // Grant consent
window.ninetailed.consent(false); // Revoke consentWhen to use: GDPR compliance. Default no-consent config only allows page events with no PII.
These plugins forward experience view events to external analytics services. All use the same default event payload template:
{
event: 'nt_experience',
ninetailed_variant: '{{selectedVariantSelector}}',
ninetailed_experience: '{{experience.id}}',
ninetailed_experience_name: '{{experience.name}}',
ninetailed_audience: '{{audience.id}}',
ninetailed_component: '{{selectedVariant.id}}',
}Available template properties: experience.id, experience.type, experience.name, experience.description, audience.id, audience.name, audience.description, selectedVariant, selectedVariantIndex, selectedVariantSelector.
| Plugin | Package | Target |
|---|---|---|
| GTM | @ninetailed/experience.js-plugin-google-tagmanager | window.dataLayer |
| Segment | @ninetailed/experience.js-plugin-segment | window.analytics.track() |
| Google Analytics | @ninetailed/experience.js-plugin-google-analytics | window.gtag() |
| Contentsquare | @ninetailed/experience.js-plugin-contentsquare | Contentsquare data layer |
// GTM
new NinetailedGoogleTagmanagerPlugin({ template?: Template })
// Segment
new NinetailedSegmentPlugin({ analytics?: AnalyticsBrowserLike, template?: Template })
// Contentsquare
new NinetailedContentsquarePlugin({ actionTemplate?: string })The full public interface exposed by all frameworks:
interface NinetailedInstance {
page: Page;
track: Track;
trackComponentView: TrackComponentView;
trackVariableComponentView: TrackVariableComponentView;
identify: Identify;
batch: Batch;
reset: Reset;
debug: Debug;
profileState: ProfileState;
onProfileChange: OnProfileChange;
onChangesChange: OnChangesChange;
plugins: NinetailedPlugin[];
logger: Logger;
eventBuilder: EventBuilder;
onIsInitialized: OnIsInitialized;
observeElement: ObserveElement;
unobserveElement: UnObserveElement;
onSelectVariant: OnSelectVariant;
}type Storage = {
getItem: (key: string) => any;
setItem: (key: string, value: any) => void;
removeItem: (key: string) => void;
};type NinetailedRequestContext = {
url: string;
referrer: string;
locale: string;
userAgent: string;
document?: { title: string };
};type SessionStatistics = {
id: string;
isReturningVisitor: boolean;
landingPage: Page;
count: number;
activeSessionLength: number;
averageSessionLength: number;
};type ProfileWithSelectedVariants = {
profile: Profile;
experiences: SelectedVariantInfo[];
changes: Change[];
};type Event =
| PageviewEvent
| TrackEvent
| IdentifyEvent
| ScreenEvent
| ComponentViewEvent
| ComponentClickEvent
| ComponentHoverEvent;type EventType = 'page' | 'track' | 'identify' | 'screen' | 'component' | 'component_click' | 'component_hover';enum ComponentTypeEnum {
EntryReplacement = 'EntryReplacement',
InlineVariable = 'InlineVariable',
}
type EntryReplacement<Variant extends Reference> = {
type: ComponentTypeEnum.EntryReplacement;
baseline: Baseline;
variants: (Variant | VariantRef)[];
};
type InlineVariable = {
type: ComponentTypeEnum.InlineVariable;
key: string;
valueType: 'String' | 'Object' | 'Boolean' | 'Number';
baseline: { value: AllowedVariableType };
variants: { value: AllowedVariableType }[];
};
type AllowedVariableType = string | boolean | number | JsonObject;Package: @ninetailed/experience.js-node
For server-side track and identify events (bulk imports, serverless functions). Uses the batch endpoint.
import { NinetailedAPIClient } from '@ninetailed/experience.js-node';
const apiClient = new NinetailedAPIClient({
clientId: 'YOUR_API_KEY',
environment: 'YOUR_NINETAILED_ENV',
});
apiClient.sendTrackEvent(id, eventName, properties?, options?);
apiClient.sendIdentifyEvent(id, traits, options?);
apiClient.getProfile(id, options?);Options: { anonymousId?: string, timestamp?: number, timeout?: number }
No page function is exposed. For ESR/SSR page events, use the Shared SDK (NinetailedApiClient).
// Cookie name for anonymous ID persistence (SSR plugin)
const NINETAILED_ANONYMOUS_ID_COOKIE = '__ninetailed_preview_id';
// Experience trait prefix
const EXPERIENCE_TRAIT_PREFIX = 'nt_experiment_';This file