Home / Guides / Sensors
Product guide

Sensors

A sensor turns a piece of text in your app into a measurement point. Because ContentFlow already serves your app's localized strings, any string key, such as a button label, an eligibility message or an onboarding step title, can be switched on as a sensor from the dashboard. From then on the app records when that text was seen and when someone interacted with it, without a new release and without inventing event names.

In short
  • Switch a string key on as a sensor in the portal, and the app records when that text is seen and tapped, with no release.
  • Load strings through the SDK, and record taps with the SDK's string-interaction call (trackString in JavaScript).
  • Relationships (ordered, absence, funnel) find patterns over time, and rules add the people they match to a segment.
  • The most common trap: the SDK emits nothing for a key that isn't an enabled sensor, or without analytics consent.
Why teams use sensors

Business use cases

Conversion

Checkout page health

Track the CTA, the APR disclosure and the eligibility message on one page: how many saw each, how many tapped.

Onboarding

Step-by-step journey

Welcome → step 1 → card activation as an ordered tracker, showing where people stop.

Win-back

Saw eligibility, didn't apply

An absence relationship finds people who saw "You're eligible" but never tapped Apply within a day, and a rule adds them to a win-back segment.

Copy

Did the new message land?

Watch a single summer-launch message and compare engagement before and after a copy edit.

Concepts

Sense, Relate, Act

SenseEnable a string key as a sensor. The app emits impression and interaction signals for it.
RelateDefine how sensors relate over time: ordered (A then B), absence (A without B) or funnel (2–8 steps).
ActA rule fires when a relationship is satisfied and adds the person to a segment, which blocks, campaigns and journeys already target.

A sensor key is dormant until enabled. Trackers are saved views over sensors: a surface (several keys on a page), a journey (2–8 keys in order) or a watch (exactly one key).

In the app

Instrumenting the app

// 1. Reading a string through the SDK records a 'usage' impression
//    when the key is an enabled sensor and analytics consent is on:
applyButton.textContent = client.t('offer.apply_cta', 'Apply now');

// 2. Record an interaction (and impressions you measure yourself):
applyButton.onclick = () => client.trackString('offer.apply_cta', 'interaction', { path: '/offers' });

// 3. Web: impressions when the element is at least 50% visible
import { bindStringSensors } from '@contentflow/sdk/web';
bindStringSensors(client);          // watches [data-cf-t="offer.apply_cta"] elements
import { useEffect } from 'react';
import { useCF, useCFTranslations } from '@contentflow/sdk/react';
import { bindStringSensors } from '@contentflow/sdk/web';

function ApplyButton() {
  const cf = useCF();
  useCFTranslations();   // re-render when the catalog loads or changes

  // 1. Reading a string through cf.t() records a 'usage' impression when the key is
  //    an enabled sensor and analytics consent is on. useCFString() does not.
  const label = cf.t('offer.apply_cta', 'Apply now');

  // 2. Record an interaction (and impressions you measure yourself):
  return <button onClick={() => cf.trackString('offer.apply_cta', 'interaction', { path: '/offers' })}>{label}</button>;
}

// 3. Web: bind once near the root. Elements marked data-cf-t="offer.apply_cta" get an
//    impression when at least 50% visible, and an interaction on every click, so they
//    need no onClick from step 2.
function StringSensors() {
  const cf = useCF();
  useEffect(() => {
    const sensors = bindStringSensors(cf);
    return () => sensors.dispose();
  }, [cf]);
  return null;
}
import { Pressable, Text } from 'react-native';
import { useCF, useCFTranslations } from '@contentflow/sdk/react-native';

function ApplyButton() {
  const cf = useCF();
  useCFTranslations();   // re-render when the catalog loads or changes

  // 1. Reading a string through cf.t() records a 'usage' impression when the key is
  //    an enabled sensor and analytics consent is on. useCFString() does not.
  const label = cf.t('offer.apply_cta', 'Apply now');

  // 2. Record an interaction. Outside a browser there is no page path unless you pass one.
  const onPress = () => cf.trackString('offer.apply_cta', 'interaction', { path: '/offers' });

  // 3. No DOM binder in React Native: when your own visibility check sees the element,
  //    report it with cf.trackString('offer.apply_cta', 'impression', { source: 'viewport' }).
  return <Pressable onPress={onPress}><Text>{label}</Text></Pressable>;
}
// 1. Load the catalog (it carries the enabled sensor keys) and read the text.
//    Swift: reading a string records nothing, so record the impression yourself.
let strings = try await client.getStrings()
applyButton.setTitle(strings.string("offer.apply_cta", fallback: "Apply now"), for: .normal)
try await client.trackSensorImpression(key: "offer.apply_cta")

// 2. Record an interaction when the person taps:
try await client.trackSensorInteraction(key: "offer.apply_cta", path: "/offers")
// 1. Load the catalog (it carries the enabled sensor keys), then read the text.
//    getString records an impression when the key is an enabled sensor
//    and analytics consent is on.
client.extensions.fetchStrings()
applyButton.text = client.extensions.getString("offer.apply_cta", "Apply now")

// 2. Record an interaction when the person taps (Kotlin takes a screen name, not a path):
applyButton.setOnClickListener {
    client.extensions.trackStringInteraction("offer.apply_cta", screen = "offers")
}
final cf = ContentFlow.instance;

// 1. Load the catalog (it carries the enabled sensor keys) and read the text.
//    Flutter: reading the map records nothing, so record the impression yourself.
final strings = await cf.getStrings();
final label = strings['offer.apply_cta'] ?? 'Apply now';
await cf.trackStringImpression('offer.apply_cta');

// 2. Record an interaction when the person taps:
await cf.trackStringInteraction('offer.apply_cta', path: '/offers');

The SDK only emits signals for keys in the catalog's smartKeys, with analytics consent. How a key is switched on and off (in the dashboard, or with disableStrings), when that reaches apps, and what each SDK records are explained once in Localization & strings: sensors.

How it works

Signal sequence

Enable a sensor → signal → rule → segment
  1. In the dashboard, under Sensors. Enabling may need approval.
  2. The SDK refreshes its strings catalog.

    If this step is skipped: Until the app refreshes its strings catalog it doesn't know the key is a sensor and emits nothing for it. When each SDK refreshes is in Localization & strings.

  3. The catalog carries the enabled sensor keys. The server caches it for up to 60 s.
  4. The SDK now knows which keys to report.
  5. Key, locale, namespace, path and source (usage or viewport), batched with other events.

    If this step is skipped: Without analytics consent the signal is dropped in the SDK (consent_not_granted). Calling track('string_impression') directly is dropped too: use t() or trackString().

  6. Validated (key up to 256 characters) and stored.
  7. The rule engine checks the active rules and relationships against new signals.
  8. ContentFlow adds the person to the segment. Within the rule's limits: per-minute and per-day caps, at most once per person per 24 hours by default, and no actions on signals older than an hour.
Relate

Trackers

Trackers show views, interactions and engagement rate (interactions ÷ views) per key, live presence, roll-ups over time, and funnels for journey trackers. When interactions exceed views (for example, the text was read without a viewport binding) the rate is shown as over 100%.

Relate

Relationships

TypeNodesAnswers
ordered2Did people do A, then B, within the window?
absence2Who did A but not B within the window?
funnel2–8How many reached each step? (diagnostic only; rules can't use funnels)

Each node is a key plus a signal (impression or interaction). Windows run from 1 second to 7 days. A relationship whose keys are no longer sensors shows as broken.

Act

Rules

A rule has one trigger (relationship satisfied, pinned to a relationship version) and one action (add to segment). For an absence relationship, remove on recovery takes the person out again when they complete the step. Rules are drafted, activated (with approval where your workspace requires it, answered 202 submittedForApproval), paused and archived; every change is kept as a revision. A workspace kill switch stops all rule actions at once, and up to 500 rules can be active.

Rules feed segments, not campaigns directly. Point a journey's segment_entry trigger or a block's segment at the rule's segment to act on it.

Results

What you see

The Sensors dashboard lists each key's impressions, interactions and interaction rate (up to 500 keys), with workspace totals. Rule pages show a summary and every execution.

Troubleshooting

When it doesn't work

SituationResult
Key isn't an enabled sensorThe SDK emits nothing for it
No analytics consentSignals dropped in the SDK, reported once per key
Invalid signal (key over 256 characters, bad source)Dropped with invalid_string_signal
Signal sent anyway for a key that isn't an enabled sensor (an older SDK, or a direct API call)Dropped by the server with sensor_not_enabled; nothing is recorded
Enabling a key that doesn't exist404 UNKNOWN_STRING
Plan without advanced analyticsUPGRADE_REQUIRED
Rule on a funnel relationship400 RELATIONSHIP_TYPE_UNSUPPORTED
Activating a rule whose relationship is brokenRELATIONSHIP_BROKEN

Next