LogoLogo
SDK ReferenceChangelogBlogStatusGet HelpGo To Dashboard
  • Introduction
  • Basics
    • How ContextDecision Works
    • How ContextPush Works
    • Getting Started
  • Context Decision
    • Logging Conversions
    • Revenue Outcomes
      • Logging Revenue with RevenueCat
    • Adding Entry Points
    • Release Checklist
    • Advanced
      • Custom Signals
      • Alternative Outcomes
      • Custom Outcome Metadata
      • Listening for Good Moments
      • Model Distribution Methods
      • Custom A/B Test Segmentation
      • Analytics & Reporting
  • Context Push
    • Integrating ContextPush
    • Push Notification Providers
      • OneSignal
      • Customer.io
      • Simple Web Request
    • Release Checklist
    • Analytics & Reporting
  • Discover By Use Cases
    • Multivariate Monetization
    • Inline Banners
  • Other Information
    • Glossary
    • Updating Your SDK
    • Minimum SDK Requirements
    • FAQ
    • Get Help
    • Changelog
  • Advanced
    • Custom Configuration
    • Capturing Context In Key Moments
Powered by GitBook
On this page
  • Capturing Context
  • Logging Outcomes
  • Decision-Making
  • Usage Example
  • Retrieving an Existing Context
  • Usage Example
  • Advanced
  • Canceling a Pending Context Callback
  • Capturing an Instant Context

Was this helpful?

  1. Context Decision

Logging Conversions

Learn how to capture user context and log conversions to optimize upsell decisions.

Optimizing conversions begins with tracking them. In ContextDecision, this is done by capturing the user’s context and logging whether a conversion occurred.

Our SDK provides different methods for capturing context, each designed for specific use cases. This document outlines these methods and explains when to use them.

If you’re new here, refer to the instructions at Getting Started for guidance on generating your license key and installing the SDK.

Capturing Context

ContextManager.swift
static func fetchContext(
    flowName: String,
    duration: Int,
    customSignals: [CustomSignal] = [],
    callback: @escaping ((Context) -> Void)
)
ContextSDK.kt
fun fetchContext(
    flowName: String,
    durationS: Int = 3,
    customSignals: CustomSignals = CustomSignals(),
    callback: (RealWorldContext) -> Unit
)
ContextSDK.java
public void fetchContext(
    String flowName,
    int durationS,
    CustomSignals customSignals,
    Consumer<RealWorldContext> callback
)
context_sdk_platform_interface.dart
Future<int> fetchContext(
  String flowName,
  int duration,
  Map<String, dynamic>? customSignals,
)
ContextSDKBinding.cs
public static void FetchContext(
    string flowName,
    ContextDelegate callback,
    CustomSignals? customSignals = null,
    int duration = 3
)
index.tsx
export function fetchContext(options: {
  flowName: string;
  onContextReady: (context: Context) => void;
  duration?: number;
  customSignals?: CustomSignals;
}): void

This method allows you to capture the user context asynchronously. This is the recommended method to be used in most scenarios.

Always call this method, and other context-capturing methods, before displaying an in-app offer. This is because if the context is captured after showing the offer, not only ContextDecision can't make a decision on whether to show the offer or not (because it's already been shown), but also the user's context likely will have already changed (e.g. user was lying in bed but once they see an upsell offer they get up or leave the app).

When calling this method, you provide:

  • A flow name, which uniquely identifies the context being captured. Each flow should have a distinct name, even if the same flow is triggered from different parts of the app. We automatically analyze the overlap between different flows, to decide if there should be one, or more models.

    • Best practice: use snake_case and group flows that lead to the same prompt using the same prefix, e.g. upsell_onboarding, upsell_first_action .

  • A duration, which determines how long accelerometer and gyroscope data is collected, in seconds. This value must be between 2 and 7 seconds, with a recommended default of 3 seconds.

  • An optional array of custom signals, which allows you to append additional contextual information relevant to your flow or app. Learn more in Custom Signals.

Once the context is ready, the callback executes asynchronously on the main thread. The execution timing depends on your app’s state:

  • If the app has been active and in the foreground for at least 3 seconds (or your configured duration), the callback executes instantly.

  • If the app was recently launched or resumed from the background, it may take up to 3 seconds (or your configured duration) for the context to be available.

Logging Outcomes

An outcome indicates whether an offer led to a conversion (e.g., a purchase, an ad click) or was dismissed. Always log at least one outcome for each captured context to help train the ML models effectively.

For in-app purchases, we require the product that was purchased to be passed as an argument to the outcome log. See Revenue Outcomes for more details.

For advanced outcome options, see Alternative Outcomes and Custom Outcome Metadata.

Decision-Making

There are exceptions, such as the Multivariate Monetization use case, where you would show an ad (not a paywall) when shouldUpsell is false. In these cases, you shouldn't log the skipped outcome, but instead log the outcome of your ad interaction.

For more details about this use case, see Multivariate Monetization.

Usage Example

To use this method, show the upsell offer inside the callback block to ensure the context is evaluated before presenting the offer. This guarantees that the decision logic runs first, preventing unnecessary offers from being shown when the conditions are not met:

MyOnboardingViewController.swift
// 1. Capture the context
ContextManager.fetchContext(flowName: "upsell_onboarding") { [weak self] context in
    // 2. Check if shouldUpsell is true
    guard context.shouldUpsell else {
        context.log(.skipped) // Log skipped when shouldUpsell is false
        return
    }
    // 3. Create the offer view controller
    let vc = MyPremiumOfferViewController()
    vc.userDidPurchase = { product in
        context.logRevenueOutcome(from: product)
    }
    vc.userDidDismiss = {
        context.log(.negative)
    }
    // 4. Show offer view controller here
    // …
}
MyOnboardingFragment.kt
// 1. Capture the context
ContextSDK.fetchContext("upsell_onboarding") { context ->
    // 2. Check if shouldUpsell is true
    if (!context.shouldUpsell) {
        context.log(EventOutcome.SKIPPED) // Log skipped when shouldUpsell is false
        return@fetchContext
    }
    // 3. Create the offer fragment
    val fragment = MyPremiumOfferFragment().apply {
        setOnUserDidPurchaseListener { product ->
            context.log(EventOutcome.POSITIVE)
        }
        setOnUserDidDismissListener {
            context.log(EventOutcome.NEGATIVE)
        }
    }
    // 4. Show offer fragment here
    // …
}
MyOnboardingFragment.java
// 1. Capture the context
ContextSDK.Companion.fetchContext("upsell_onboarding", null, null, context -> {
    // 2. Check if shouldUpsell is true
    if (!context.getShouldUpsell()) {
        context.log(EventOutcome.SKIPPED); // Log skipped when shouldUpsell is false
        return;
    }
    // 3. Create the offer fragment
    MyPremiumOfferFragment fragment = new MyPremiumOfferFragment();
    fragment.setOnUserDidPurchaseListener(product -> context.log(EventOutcome.POSITIVE));
    fragment.setOnUserDidDismissListener(() -> context.log(EventOutcome.NEGATIVE));
    // 4. Show offer fragment here
    // …
    return Unit.INSTANCE;
});
my_onboarding_screen.dart
// 1. Capture the context
_contextSdkPlugin.fetchContext("upsell_onboarding", 3).then((context) async {
  // 2. Check if shouldUpsell is true
  if (!(await context.shouldUpsell)) {
      await context.log(Outcome.skipped); // Log skipped when shouldUpsell is false
      return;
  }
  // 3. Create the offer screen
  final offerScreen = PremiumOfferScreen(
    onPurchase: (product) {
      await context.log(Outcome.positive);
    },
    onDismiss: () {
      await context.log(Outcome.negative);
    },
  );
  // 4. Show offer screen here
  // …
});
MyOnboardingScreen.cs
// 1. Capture the context
ContextSDKBinding.FetchContext("upsell_onboarding", delegate (Context context) {
    // 2. Check if shouldUpsell is true
    if (!context.shouldUpsell) {
        context.Log(Outcome.Skipped); // Log skipped when shouldUpsell is false
        return;
    }
    // 3. Create the offer screen
    var offerScreen = new PremiumOfferScreen();
    offerScreen.OnPurchase = product =>
    {
        context.Log(Outcome.Positive);
    };
    offerScreen.OnDismiss = () =>
    {
        context.Log(Outcome.Negative);
    };
    // 4. Show offer screen here
    // …
});
MyOnboardingScreen.js
import { fetchContext, Outcome } from 'react-native-context-sdk';

// 1. Capture the context
fetchContext({
  flowName: 'upsell_onboarding',
  onContextReady: async (context) => {
    // 2. Check if shouldUpsell is true
    if (!(await context.shouldUpsell())) {
        await context.log(Outcome.skipped); // Log skipped when shouldUpsell is false
        return;
    }
    // 3. Create and navigate to the offer screen
    navigation.navigate('PremiumOffer', {
      onPurchase: (product: any) => {
        context.log(Outcome.positive);
      },
      onDismiss: () => {
        context.log(Outcome.negative);
      },
    });
  },
});

If your project does not favor closure-based implementations like shown above when logging outcomes, see Retrieving an Existing Context below for an alternative approach that better fits different architectures.

Retrieving an Existing Context

Not all architectures use closures for inter-view-controller communication. In such cases, you can capture the context and present the offer in one place, and log the outcome separately. Use the recentContext(flowName:) method to retrieve a previously captured context for a given flow name.

Note: This method does not capture a new context — it only retrieves an existing one. If no context has been captured for the specified flow, this method returns nil.

Usage Example

MyOnboardingViewController.swift
ContextManager.fetchContext(flowName: "upsell_onboarding") { [weak self] context in
    guard context.shouldUpsell else {
        context.log(.skipped)
        return
    }
    self?.present(MyPremiumOfferViewController(), animated: true)
}
MyPremiumOfferViewController.swift
func userDidCompletePurchase(product: Product) {
    if let context = ContextManager.recentContext(flowName: "upsell_onboarding") {
        context.logRevenueOutcome(from: product)
    } else {
        // This is an error state. Make sure the context above is created first.
    }
}

func dismissViewController() {
    if let context = ContextManager.recentContext(flowName: "upsell_onboarding") {
        context.log(.negative)
    } else {
        // This is an error state. Make sure the context above is created first.
    }
    // Dismiss your view controller here
}
MyOnboardingFragment.kt
ContextSDK.fetchContext("upsell_onboarding") { context ->
    if (!context.shouldUpsell) {
        context.log(EventOutcome.SKIPPED)
        return@fetchContext
    }
    val fragment = MyPremiumOfferFragment()
    // Show fragment here
}
MyPremiumOfferFragment.kt
fun userDidCompletePurchase(product: Product) {
    val context = ContextSDK.recentContext("upsell_onboarding")
    if (context != null) {
        context.log(EventOutcome.POSITIVE)
    } else {
        // This is an error state. Make sure the context above is created first.
    }
}

fun dismissFragment(fragment: Fragment) {
    val context = ContextSDK.recentContext("upsell_onboarding")
    if (context != null) {
        context.log(EventOutcome.NEGATIVE)
    } else {
        // This is an error state. Make sure the context above is created first.
    }
    // Dismiss the fragment here
}
MyOnboardingFragment.java
ContextSDK.Companion.fetchContext("upsell_onboarding", null, null, context -> {
    if (!context.getShouldUpsell()) {
        context.log(EventOutcome.SKIPPED);
        return;
    }
    MyPremiumOfferFragment fragment = new MyPremiumOfferFragment();
    // Show fragment here
    return Unit.INSTANCE;
});
MyPremiumOfferFragment.java
public void userDidCompletePurchase(Product product) {
    RealWorldContext context = ContextSDK.Companion.recentContext("upsell_onboarding");
    if (context != null) {
        context.log(EventOutcome.POSITIVE);
    } else {
        // This is an error state. Make sure the context above is created first.
    }
}

public void dismissFragment(Fragment fragment) {
    RealWorldContext context = ContextSDK.Companion.recentContext("upsell_onboarding");
    if (context != null) {
        context.log(EventOutcome.NEGATIVE);
    } else {
        // This is an error state. Make sure the context above is created first.
    }
    // Dismiss the fragment here
}
my_onboarding_screen.dart
_contextSdkPlugin.fetchContext("upsell_onboarding", 3).then((context) async {
  if (!(await context.shouldUpsell)) {
      await context.log(Outcome.skipped);
      return;
  }
  // Show offer screen here
  // …
});
my_premium_offer_screen.dart
void userDidCompletePurchase(Product product) async {
  final context = await _contextSdkPlugin.recentContext("upsell_onboarding");
  if (context != null) {
    await context.log(Outcome.positive);
  } else {
    // This is an error state. Make sure the context above is created first.
  }
}

void dismissView(BuildContext context) async {
  final context = await _contextSdkPlugin.recentContext("upsell_onboarding");
  if (context != null) {
    await context.log(Outcome.negative);
  } else {
    // This is an error state. Make sure the context above is created first.
  }
  // Dismiss screen here
}
MyOnboardingScreen.cs
// 1. Capture the context
ContextSDKBinding.FetchContext("upsell_onboarding", delegate (Context context) {
    // 2. Check if shouldUpsell is true
    if (!context.shouldUpsell) {
        context.Log(Outcome.Skipped);
        return;
    }
    // 3. Show offer screen here
    // …
});

</div>

<div data-gb-custom-block data-tag="code" data-title='MyPremiumOfferScreen.cs'>

```cs
public void UserDidCompletePurchase(Product product)
{
    var context = ContextSDKBinding.RecentContext("upsell_onboarding");
    if (context != null)
    {
        context.Log(Outcome.Positive);
    }
    else
    {
        // This is an error state. Make sure the context above is created first.
    }
}

public void DismissView()
{
    var context = ContextSDKBinding.RecentContext("upsell_onboarding");
    if (context != null)
    {
        context.Log(Outcome.Negative);
    }
    else
    {
        // This is an error state. Make sure the context above is created first.
    }

    // Dismiss screen here
}
MyOnboardingScreen.js
import { fetchContext, Outcome } from 'react-native-context-sdk';

fetchContext({
  flowName: 'upsell_onboarding',
  onContextReady: async (context) => {
    if (!(await context.shouldUpsell())) {
        await context.log(Outcome.skipped);
        return;
    }
    navigation.navigate('PremiumOffer');
  },
});
MyPremiumOfferScreen.js
import { recentContext, Outcome } from 'react-native-context-sdk';

const userDidCompletePurchase = async (product) => {
  const context = await recentContext("upsell_onboarding");
  if (context) {
    await context.log(Outcome.positive);
  } else {
    // This is an error state. Make sure the context above is created first.
  }
};

const dismissScreen = async (navigation) => {
  const context = await recentContext("upsell_onboarding");
  if (context) {
    await context.log(Outcome.negative);
  } else {
    // This is an error state. Make sure the context above is created first.
  }
  // Dismiss your screen here
};

Advanced

Canceling a Pending Context Callback

The cancelContextCallback(flowName:) method allows you to cancel a pending context evaluation for a specific flow. Use this method if you have previously called one of the asynchronous methods above but no longer need the resulting context.

This is particularly useful in cases where the user navigates away from the screen before the context evaluation completes. Since context capturing is asynchronous, the callback may be executed at a later time, potentially in an unintended screen or state. Calling cancelContextCallback(flowName:) prevents this from happening by discarding the pending callback if it hasn’t already been triggered.

Usage Example

If you use context evaluation to decide whether to display an upsell offer, but the user leaves the screen before the prompt appears, you can call:

ContextManager.cancelContextCallback(for: "upsell")

Not yet supported in Android. Reach out to our team to request this feature.

Not yet supported in Android. Reach out to our team to request this feature.

Not yet supported in Flutter. Reach out to our team to request this feature.

Not yet supported in Unity. Reach out to our team to request this feature.

Not yet supported in React Native. Reach out to our team to request this feature.

This ensures the upsell offer is never shown, which helps maintain a seamless user experience by preventing outdated context-based actions from executing at inappropriate times.

Capturing an Instant Context

Use the instantContext(flowName:) method when context must be captured immediately, with no tolerance for delays:

ContextManager.swift
@discardableResult
static func instantContext(
    flowName: String,
    duration: Int,
    customSignals: [CustomSignal] = []
) -> Context
ContextSDK.kt
fun instantContext(
    flowName: String,
    durationS: Int = 3,
    customSignals: CustomSignals = CustomSignals(),
): RealWorldContext
ContextSDK.java
public RealWorldContext instantContext(
    String flowName,
    int durationS,
    CustomSignals customSignals
)
context_sdk_platform_interface.dart
Future<int> instantContext(
  String flowName,
  int duration,
  Map<String, dynamic>? customSignals,
)
ContextSDKBinding.cs
public static Context InstantContext(
    string flowName,
    CustomSignals? customSignals = null,
    int duration = 3
)
index.tsx
export async function instantContext(options: {
  flowName: string;
  duration?: number;
  customSignals?: CustomSignals;
}): Promise<Context>

This method is ideal for user-initiated triggers, such as:

  • Determining whether to display an upsell offer when the user presses a button to navigate to the next screen.

  • Capturing context when the user presses a button to show the subscription flow. This can be useful to provide additional data for ML models.

In those cases, waiting up to 3 seconds is not an option, as it would seem like the app became unresponsive (at least if done without showing a loader).

The down side of this method is that if the app hasn't been in the foreground for at least 3 seconds, the quality of the signals captured may be poor and thus unsuitable for ML training. Thus, if this method needs to be used, keep in mind that it might end up taking longer to capture quality data to train your models.

Usage Example

MyOnboardingViewController.swift
let context = ContextManager.instantContext(flowName: "upsell_onboarding", duration: 3)
if context.shouldUpsell {
    let vc = MyPremiumOfferViewController()
    vc.userDidPurchase = { product in
        context.logRevenueOutcome(from: product)
    }
    vc.userDidDismiss = {
        context.log(.negative)
    }
    present(vc, animated: true)
} else {
    context.log(.skipped)
}
MyOnboardingFragment.kt
val context = ContextSDK.instantContext("upsell_onboarding", durationS = 3)
if (context.shouldUpsell) {
    val fragment = MyPremiumOfferFragment().apply {
        setOnUserDidPurchaseListener { product ->
            context.log(EventOutcome.POSITIVE)
        }
        setOnUserDidDismissListener {
            context.log(EventOutcome.NEGATIVE)
        }
    }
    // Show the fragment here
    // …
} else {
    context.log(EventOutcome.SKIPPED)
}
MyOnboardingFragment.java
RealWorldContext context = ContextSDK.Companion.instantContext("upsell_onboarding", 3, null);

if (context.getShouldUpsell()) {
    MyPremiumOfferFragment fragment = new MyPremiumOfferFragment();

    fragment.setOnUserDidPurchaseListener(product -> context.log(EventOutcome.POSITIVE));
    fragment.setOnUserDidDismissListener(() -> context.log(EventOutcome.NEGATIVE));

    // Show the fragment here
    // …
} else {
    context.log(EventOutcome.SKIPPED);
}
my_onboarding_screen.dart
final context = await _contextSdkPlugin.instantContext("upsell_onboarding", 3);
if (await context.shouldUpsell()) {
  final offerScreen = PremiumOfferScreen(
    onPurchase: (product) {
      await context.log(Outcome.positive);
    },
    onDismiss: () {
      await context.log(Outcome.negative);
    },
  );
  // Show offer screen here
  // …
} else {
  await context.log(Outcome.skipped);
}
MyOnboardingView.cs
Context context = ContextSDKBinding.InstantContext("upsell_onboarding");
if (context.shouldUpsell) {
    PremiumOfferScreen offerScreen = new PremiumOfferScreen(
        onPurchase: (Product product) => context.Log(Outcome.Positive),
        onDismiss: () => context.Log(Outcome.Negative)
    );
    // Show offer screen here
    // …
} else {
    context.Log(Outcome.Skipped);
}
MyOnboardingScreen.js
import { instantContext, Outcome } from 'react-native-context-sdk';

const context = await instantContext({ flowName: 'upsell_onboarding' });
if (await context.shouldUpsell()) {
  navigation.navigate('PremiumOffer', {
    onPurchase: (product: any) => {
      await context.log(Outcome.positive);
    },
    onDismiss: () => {
      await context.log(Outcome.negative);
    },
  });
} else {
  await context.log(Outcome.skipped);
}
PreviousGetting StartedNextRevenue Outcomes

Last updated 20 days ago

Was this helpful?

If your offer is a paywall with a "Restore Purchases" button, see .

The context object returned by fetchContext includes a shouldUpsell property, which determines whether an upsell offer should be shown. During the , this property always returns true. Once a ML model is deployed to the flow, it starts making real-time decisions. If the model determines that it's a bad time, shouldUpsell will be false, so you can not show the paywall, and thus log skipped as the outcome.

calibration phase
The skipped outcome