handleLiveTestCampaign method
void
handleLiveTestCampaign(
- LiveTestInvocation invocation
Handles one campaign_test SSE event: ACKs received, then routes the
inline campaign through the same _routeStoredCampaign path an organic
CEP trigger uses.
Implementation
void handleLiveTestCampaign(LiveTestInvocation invocation) {
LiveTestAckReporter.instance.postReceived(invocation.testInvocationId);
// Rare but possible: the live-test stream can connect before the
// campaign fetch resolves. Guard like onCampaignTriggered does.
if (_sdkState != SDKState.ready) {
LiveTestAckReporter.instance.postFailed(
invocation.testInvocationId,
LiveTestFailureCode.renderError,
'SDK not ready (state=${_sdkState.name})',
);
return;
}
final campaignJson = invocation.campaign;
if (campaignJson == null) {
LiveTestAckReporter.instance.postFailed(
invocation.testInvocationId,
LiveTestFailureCode.campaignNotFound,
'campaign_test message had no usable campaign object',
);
return;
}
// With the bundle's catalog, exactly as the fetched campaigns are parsed.
// Parsing a live test against an empty one resolved every design token to
// nothing, so a campaign styled with tokens arrived on the device with no
// colours and no type — the one place a marketer looks at it before
// shipping. Android has always passed its catalog here.
final campaign = CampaignModel.fromJson(
campaignJson,
designTokens: _campaignDesignTokens,
serverClock: _serverTimeClock,
);
if (campaign == null) {
LiveTestAckReporter.instance.postFailed(
invocation.testInvocationId,
LiveTestFailureCode.templateError,
'campaign object could not be parsed into a renderable campaign',
);
return;
}
final activeGuidePayload = _guideOrchestrator.state?.payload;
if (activeGuidePayload != null &&
isLiveTestCepId(activeGuidePayload.cepCampaignId)) {
_guideManager.dismiss();
}
if (campaign.config is PipCampaignConfig ||
campaign.config is FloaterStoryCampaignConfig) {
_replaceActiveLiveTestFloater();
}
// CEPTriggerPayload.variables is Map<String, String> (every trigger path
// shares this shape); the wire payload's variables are arbitrary JSON.
final coercedVariables = <String, String>{
for (final entry in invocation.variables.entries)
entry.key: entry.value.toString(),
};
final cepCampaignId = liveTestCepId(invocation.testInvocationId);
final payload = CEPTriggerPayload(
cepCampaignId: cepCampaignId,
cepMetadata: const {},
campaignKey: campaign.campaignKey,
variables: coercedVariables,
);
// Also clears the impression dedup mark for this id — otherwise it'd
// leak one entry per live-tested inline campaign for the process's life.
void cleanUpLiveTestState() {
_liveTestContexts.remove(cepCampaignId);
_liveTestCampaigns.remove(cepCampaignId);
_events.resetImpression(cepCampaignId);
}
final testContext = LiveTestContext(
testInvocationId: invocation.testInvocationId,
onTerminal: cleanUpLiveTestState,
);
_liveTestContexts[cepCampaignId] = testContext;
_liveTestCampaigns[cepCampaignId] = campaign;
final accepted = _routeStoredCampaign(
campaign,
payload,
context: _LiveTestRoutingContext(testContext),
);
if (!accepted) {
// Drop branches already report their own failure; this just cleans up.
cleanUpLiveTestState();
}
}