The Storyteller SDK fires analytics events for various user interactions. You can subscribe to these events to track user engagement, forward analytics to your own systems, or implement custom behavior based on user actions.
Subscribe before mounting Storyteller content, retain the subscription, and
cancel it when its owning service or widget is disposed. Events are live; the
stream does not replay interactions that occurred before the listener.
To receive analytics events, subscribe to the onUserActivityOccurred stream provided by the SDK:
import'package:storyteller_sdk/storyteller_sdk.dart';// Subscribe to all user activity eventsStoryteller.onUserActivityOccurred.listen((event){print('Event Type: ${event.type}');print('Story ID: ${event.data.storyId}');print('Story Title: ${event.data.storyTitle}');// Forward to your analytics system// Pass a map to your analytics provider, for example:// analytics.track(event.type, event.data.fields);});
Each event is a StorytellerUserActivityEvent containing:
type - The event type as a String (e.g., 'openedStory', 'completedPage')
data - A StorytellerUserActivityData object with detailed information about the event
type is the unmodified native string, and data.fields contains the complete
payload delivered to Dart by the platform bridge. Typed Dart properties are
conveniences for known fields; preserve the raw values when forwarding
analytics so delivered keys without a typed Flutter property remain usable.
For more information on implementing event streams, see the Storyteller Delegate page.
Use the exact native wire strings below when comparing event.type; Flutter
does not rename them. For example, the Opened Story heading corresponds to
openedStory. Shared action/share event names apply to Stories and Clips; use
the delivered Story/Clip fields to identify their context.
This guide describes Flutter integration behavior. The governed cross-platform
Analytics reference is maintained separately and its publication is independent
of an SDK release. Event availability depends on the package/native versions;
see the Changelog before adopting Unreleased events.
In the discussion below, completing a Page means allowing its playback/timer
to finish: the configured duration for an image Page or the playback duration
for a video. Do not assume a fixed 15-second image duration.
This event is emitted when a Story or Clip list tile becomes visible through an
initial render, scrolling, or a true list reload/data refresh. tileIndex is
the one-based visible tile position. The optional context map carries the
integrator context associated with the list. Android may additionally supply
originalPosition and displayPosition for reordered lists.
sdkInitialized is internal Storyteller telemetry. Android and iOS exclude it
from customer analytics callbacks, so it does not arrive through
onUserActivityOccurred. Use the result returned by
await Storyteller.initialize(...) to check initialization success. The
SDK Initialization Fields retained on the Dart
data type do not change this delivery rule.
Android declares unsupported, and the native SDK may emit a newer event
name that is not listed
on this page. Flutter deliberately retains that string in event.type, so no
Dart enum update is required to receive it. Every key delivered by the platform
bridge is retained in event.data.fields. iOS's Codable bridge naturally
includes newly encoded keys; Android native payload additions still require the
Flutter Android mapper to be updated during the corresponding native SDK bump.
This event is recorded in the following scenarios:
When a user taps on a row item to open a story
When a story is loaded because the previous Story finished
When a story is loaded because the user tapped to skip the last page of the previous story
When a user swipes left on a story to go to the next story
When a user swipes right on a story to go to the previous story
When a user is sent directly to a story via a deep link
Whenever an Opened Story event occurs, additional event data specific to this event includes storyReadStatus, categories, categoryDetails, storyIndex, and openedReason.
This event is recorded whenever a user views content from a page.
openedPage identifies a Page being opened, including image and interactive
Pages. It does not by itself prove that video playback started; use
mediaStarted for the native playback-start signal.
Whenever an Opened Page event occurs, additional event data specific to this event includes openedReason.
This event is recorded when a user answers a question in a trivia quiz.
Whenever a Trivia Quiz Question Answered event occurs, additional event data specific to this event includes triviaQuizId, triviaQuizTitle, triviaQuizQuestionId, and triviaQuizAnswerId.
This event is recorded when a user completes a trivia quiz.
Whenever a Trivia Quiz Completed event occurs, additional event data specific to this event includes triviaQuizId, triviaQuizTitle, and triviaQuizScore.
a user taps on the back button in the top-left to exit the Clips player (and is at the top of the stack of Clip Categories)
This event does not fire when the user is not at the top of the Clip Category stack.
Whenever a Dismissed Clip event occurs, additional event data specific to this event includes dismissedReason, durationViewed, clipsViewed, and loopsViewed.
a user taps the primary action button at the bottom of a Clip
a user taps any secondary action buttons above the clip title
a user swipes left on a clip to open the relevant action
Whenever an Action Button Tapped event occurs, additional event data specific to this event includes actionText, actionClass, actionIndex, tappedClipActionText, tappedClipActionUrl, and tappedClipActionType.
a user taps on the screen whilst a clip is playing to pause the clip - it does not fire when a clip is paused automatically by sharing or following an action
Note: For live clips this event will never be fired, as pausing/resuming live clips is not supported.
a user scrubs the clip to a different position by dragging the clip progress bar
Whenever a Scrubbed Clip event occurs, additional event data specific to this event includes openedReason, startPosition, endPosition, and scrubDirection.
the alert dialogue box associated with the Followable Category Limit is shown
Whenever a Followable Category Limit Shown event occurs, additional event data specific to this event includes followableCategoryLimitDialogue, followableCategoryLimitActionText, and followableCategoryLimitActionUrl.
a user taps on the customisable action button in the Followable Category Limit Dialogue Box
Whenever a Followable Category Limit Action Button Tapped event occurs, additional event data specific to this event includes followableCategoryLimitDialogue, followableCategoryLimitActionText, and followableCategoryLimitActionUrl.
a user dismisses the Followable Category Limit Dialogue Box by tapping the cancel action button
Whenever a Followable Category Limit Dismissed event occurs, additional event data specific to this event includes followableCategoryLimitDialogue, followableCategoryLimitActionText, and followableCategoryLimitActionUrl.
Video Cards emit playback-start, active-watch-time heartbeat, and completion
events. durationViewed is milliseconds; videoStartReason identifies the
start reason when available. Audio state changes supply audioToggleFrom,
audioToggleTo, and isMuted. Image Cards do not emit video playback events.
These events are also available in Flutter 11.6.5.
This event is recorded at the same time as Dismissed Ad, Skipped Ad and Viewed Ad Page Complete and gives an easier way to determine when an ad finishes for any reason.
This event is recorded when a user pauses a clip which is an ad by tapping the screen. It does not fire when a clip is paused automatically for sharing or following an action.
This event is fired when a clip which is an ad is paused and a user taps the screen to resume playback. It does not fire when a clip is resumed automatically.
These native events describe an eligible ad opportunity, request, successful
load, load failure, or paid impression. They depend on the configured ad
integration and provider; an opportunity does not guarantee a request, fill,
or revenue. Correlate delivered playerSessionId, adRequestId, adSlotType,
adUnitId, source/error fields, and paid value/currency fields when present.
This event summarises the ad session after the native SDK has completed its session-level accounting. When present, the payload can include counts for opportunities, requests, loads, failed loads, paid events, and total revenue micros.
Flutter 12.1.2 delivers these events with Android 12.2.0 and iOS 11.9.0.
They are absent from Flutter 11.6.5. Story
heartbeats describe the current non-ad Page; Clip heartbeats describe the
current Clip. Heartbeats report accumulated active watch time in milliseconds
through durationViewed, at five-second milestones. Paused or inactive
playback does not advance active watch time.
Quartile events report playback progress milestones of 25, 50, and 75 through
event.data.quartile.
Quartiles measure content progress and can be crossed by scrubbing, so they
are different from elapsed watch time. Use completedPage/completedLoop
for completion and the ad quartile events for ads.
This event is called on video pages whenever the video finishes buffering.
Note: There should be at most one Ready to Play event and one Media Started event for every page. There may be multiple Buffering Started/Buffering Ended pairs of events for an individual page. There may not always be a Buffering Ended event for every Buffering Started event as the user may choose to exit the page during buffering.
Whenever a Buffering Ended event occurs, additional event data specific to this event includes isInitialBuffering and timeSinceBufferingBegan.
These events represent native mute or Story-caption state changes. Clip
caption changes use enabledClipCaptions and disabledClipCaptions above.
Use nullable isMuted/captionsEnabled for state when supplied.
A user taps the 'Search' icon after entering a term in the Search bar (whether manually or by tapping the 'arrow' icon beside a Search suggestion to populate the search bar)
A user taps the 'Search' icon beside a Search suggestion. The Search is then performed with the suggestion as the term.
A user taps 'Apply filters' from the filters interface.
A user taps the 'arrow' icon beside a Search suggestion to populate the Search bar. This does not trigger any other event, and the user may amend the Search bar input before performing a Search.
A user taps the 'Search' icon beside a Search suggestion. This simultaneously triggers a PerformedSearch event, above.
Whenever a Used Suggestion event occurs, additional event data specific to this event includes initialInput.
A tap on a tenant-configured external Search result can supply actionType,
searchResultId, searchResultTitle, and searchResultSource, available as
typed StorytellerUserActivityData properties from Flutter 12.1.2.
External result visibility uses tileVisible when applicable.
Event data is exposed as StorytellerUserActivityData. Its typed properties
are nullable and depend on the event, content, platform, and tracking options.
A list of fields on this page is not a guarantee that each one is populated.
Numeric values are converted to the declared Dart int or double; string
lists and the typed metadata/context maps convert entries to strings.
fields preserves the original delivered values instead of those conversions.
categoryType, location, locale, and itemTitle are nullable strings whose
presence depends on the native event. playerSessionId (String?) can
correlate player/ad activity. videoStartReason (String?) can be natural
or loop for video Cards.
The Dart type also retains legacy collectionId, collectionTitle,
searchQuery, url, externalId, duration, position, error,
errorMessage, and customData properties. A retained getter does not
guarantee that the current native bridge supplies that key. In particular,
externalId is used by native Storyteller ingestion and is not forwarded as a
customer analytics field by the current Android mapper.
The fields (Map<String, dynamic>) property contains every string-keyed
payload value delivered to Dart by the platform bridge, including values which
do not have a typed Dart property. Preserve this map when forwarding events if
your integration must remain forward compatible. On Android, a newly added
native field must first be added to the Flutter plugin's explicit mapper.
The tileIndex (int?) property is the one-based position for a
tileVisible event. originalPosition and displayPosition (int?) are
Android-only positions supplied for reordered lists when available.
The context (Map<String, String>?) property contains integrator context
provided to a supported list, Card collection, or open call. Android delivers
nonempty context to customer callbacks but removes it from Storyteller API
ingestion; iOS includes it in callback payloads and encoded activity records.
Do not assume identical downstream persistence across platforms. An omitted
or empty Android context is absent from the bridge payload.
These typed fields describe internal sdkInitialized telemetry, which is
excluded from customer callbacks on Android and iOS. Their presence on the
Dart type does not make them initialization notifications. They include screenResolution, appId,
appName, deviceModel, appVersion, operatingSystem, osVersion,
deviceType, deviceBrand, minOsVersion, initializationSucceeded,
enablePersonalization, enableStorytellerTracking,
enableUserActivityTracking, enableAdTracking,
enableFullVideoAnalytics, and enableRemoteViewingStore. Android may also
provide targetOsVersion and languageVersion.
The pageType (String?) is the type of the page associated with the event. Native values include image, video, poll, triviaQuiz, and empty
when applicable.
The storyPlaybackMode (String?) value states if the story was opened during the list or in the single story mode (Storyteller static method.) When supplied, the values are list or singleStory.
The actionLinkId (String?) is the unique identifier of the action associated with the current story page, clip or card. This is not included for Ad events.
The openedReason (String?) value states how the user opened a Story or Clip. The possible values for this are:
- storyListTap: The user tapped the Story in the Story row.
- clipListTap: The user tapped the Clip in a Clip list.
- deepLink: Content was opened with deep-link attribution. The Dart API
option is StorytellerOpenReason.deeplink (channel value deeplink), while
the emitted analytics value is deepLink.
- swipe: The user swiped left or right to change the current Story.
- automaticPlayback: The user completed the previous Page.
- card: The user taps on a Card.
- clipActionButton: The user clicked on an action button in the Clips player.
- pageActionButton: The user clicked on the action button in the Story player.
- tap: The user tapped on the next or previous Story Page to navigate to this page.
- instanceMethod: Storyteller.openStory() or Storyteller.openPage() was called to open a Story or a Page.
- loop: The user completes a loop of a Clip naturally or by scrubbing to the end of the Clip's duration.
- categoryListTap / categoryBackTap: Navigation into or back from a category.
- searchTap: Content opened from Search.
- embeddedClipsOpen: Initial Embedded Clips opening.
- pullToRefresh / reloadMethod: Content opened after a refresh or reload.
openedReason can be included on openedStory, openedClip, openedPage, storyInstructionsScreenViewed, completedLoop, openedAd and openedSheet events, and on other events when provided by the
native SDK. Android uses storyInstructionScreenViewed for the instruction
screen event.
The dismissedReason (String?) value states the way the user dismissed a story or clip. The possible values for this are closeButtonTapped (the user tapped close to dismiss the story); swipedDown (the user swiped down to dismiss the story); swipedFirstStory (the user swiped the first story to dismiss it); swipedFinalStory (the user swiped the final story to dismiss it); skippedFinalPage (the user tapped to skip the final page of the final story); completedFinalPage (the user completed the final page of the final story) and backButtonTapped (the user tapped the back button to dismiss the clip).
The reason can also accompany ad dismissal. Native versions can provide
additional values, including backTapped, pageActionButton, and
deepLinkReplacement; retain the string instead of rejecting unknown values.
The durationViewed (double?) is the duration the user viewed the story or clip for in milliseconds. This is measured from the most recent openedStory or openedClip event with an Opened Reason of storyListTap, deepLink, card, pageActionButton, clipActionButton or clipListTap.
This timer is reset after any dismissedStory or dismissedClip events.
For finishedClip, this describes the active Clip viewing session in
milliseconds. Heartbeat events use accumulated active watch time instead of
the Story/player session timer described above.
The pagesViewedCount (int?) is the total number of pages a user has viewed since the most recent openedStory event with an Opened Reason of storyListTap, pageActionButton, clipActionButton, card or deepLink. This count is reset after any dismissedStory events.
The quartile (int?) is the playback progress milestone (25, 50, or 75) on
organic quartile events. actionType, searchResultId, searchResultTitle,
and searchResultSource (String?) identify a configured external Search
result. These properties are new in Flutter 12.1.2.
The adPlacement (String?) represents the placement of the ad. It can be either Between Stories, Between Pages or Between Clips. This is only included for ad events.
The adStrategy (String?) represents the strategy used to display the ads. It can have the following values: Between Stories, Between Pages, Between Stories and Pages, Between Clips.
The adResponseIdentifier (String?) represents the response identifier attached to the ad that was received from the ad provider. Used for debugging ad targeting.
The adIndex (int?) represents the order of the Ad within the displayed Ads in a Story or Clip collection (1 for the first Ad, 2 for the second, etc.). This field is included in all ad-related events.
Paid ad and mediation callbacks can include adSource, adSourceName, adSourceId, adSourceInstanceName, adSourceInstanceId, adMediationGroupName, adMediationAbTestName, adMediationAbTestVariant, adAdapterLatencyMillis, adErrorCode, adErrorDomain, adErrorMessage, adValueMicros, adCurrencyCode, and adValuePrecision.
These native events describe an eligible ad opportunity, request, successful
load, load failure, or paid impression. They depend on the configured ad
integration and provider; an opportunity does not guarantee a request, fill,
or revenue. Correlate delivered playerSessionId, adRequestId, adSlotType,
adUnitId, source/error fields, and paid value/currency fields when present.
The adSessionSummary event can include adSessionOpportunitiesCount, adSessionRequestsCount, adSessionLoadsCount, adSessionFailedToLoadCount, adSessionPaidCount, adSessionRevenueMicros, and adSessionCurrencyCode.
The isInitialBuffering (bool?) value is returned if the buffering happens at the start of playback for that page. This is only included for bufferingStarted and bufferingEnded events.
The timeSinceBufferingBegan (double?) value is the duration the current buffering lasted for in milliseconds. This is only included for bufferingEnded events.
The categories (List<String>?) value contains the category names supplied by
the native SDK for an event. It can be included on Story, Clip, Card, Ad and
Search events when their native payload provides it.
Availability depends on the event, platform, bundled native SDK version and
content context. Flutter forwards the native list without restricting it to
openedStory. Handle null when the field is absent and an empty list when no
category names are supplied; do not assume every event listed in this guide
includes it.
The triviaQuizId (String?) is the ID of the trivia quiz that was completed or answered. This is only included on triviaQuizQuestionAnswered and triviaQuizCompleted events.
The triviaQuizTitle (String?) is the title of the trivia quiz that was completed or answered. This is only included on triviaQuizQuestionAnswered and triviaQuizCompleted events.
The triviaQuizQuestionId (String?) is the ID of the trivia quiz question which was answered. This is only included on triviaQuizQuestionAnswered events.
The clipIndex (int?) is the index of the clip in the row or grid at the point it was selected or the index of the clip in the player inside the original row or grid. For Ad events, clip index refers to the index of the clip before the Ad.
The clipsViewed (int?) value is the total number of clips a user has viewed since the most recent openedClip event with an Opened Reason of clipListTap, pageActionButton, card, clipActionButton or deepLink. This count should be reset after any dismissedClip events.
The loopsViewed (int?) is for dismissedClip, the total number of loops (plays of an individual clip) a user has viewed since the most recent openedClip event with an Opened Reason of clipListTap, pageActionButton, card, clipActionButton or deepLink.
This count should be reset after any dismissedClip events. For finishedClip, the total number of loops.
The clipActionType (String?) is the type of the primary action on a clip associated with the event. If there is no action button then the value is null.
collectionTitle (String?) is a retained legacy Dart property. Do not rely
on it being populated: the current Android mapper does not supply this key.
Use the nullable containerTitle for the native container title when delivered.
Android supplies collectionId as an alias of collection; iOS does not
guarantee that alias, so use collection for the collection ID.
The categoryDetails (List<StorytellerCategoryDetail>?) is a list of Category Detail objects. The details are the name, ID, type and placement of the category.
The currentCategory (StorytellerCategoryDetail?) is the category associated
with the current placement when supplied. It exposes name, id, type,
and nullable placement.
It can be supplied on Story and ad events; check for null before reading it.
The captionsEnabled (bool?) property indicates whether captions are currently enabled for the Clips Player. This can be included in Clip analytics events and represents the state of captions at the time the event occurred.
The startPosition (int?) represents the playback position, in milliseconds, when the user started scrubbing the clip. This is only included for scrubbedClip events.
The endPosition (int?) represents the playback position, in milliseconds, when the user stopped scrubbing the clip. This is only included for scrubbedClip events.
The scrubDirection (String?) indicates whether the user scrubbed to a position forward or backward on a Clip. Possible values are forward and backward. This is only included for scrubbedClip events.
The completionType (String?) indicates how a user finished watching a Clip. Possible values are natural and scrubbed.
This is included for finishedClip and completedLoop events.
The searchTerm (String?) by which the Clips / Stories were searched, either entered in the search bar by the user or selected / filled from search suggestions.
The actionText (String?) property is used for general action text and can apply to both page actions and clip actions depending on the placement context.
The actionClass (String?) identifies whether the action button that was tapped is a primary or secondary action. Possible values are primary and secondary. This is only included for actionButtonTapped events.
The actionIndex (int?) is the 1-based index of the secondary action that was tapped. This is only included for actionButtonTapped events when a secondary action is tapped.
The tappedClipActionText (String?) is the text of the specific action button (primary or secondary) that was tapped. This is only included for actionButtonTapped events.
The tappedClipActionUrl (String?) is the URL of the specific action button (primary or secondary) that was tapped. This is only included for actionButtonTapped events.
The tappedClipActionType (String?) is the type of the specific action button (primary or secondary) that was tapped. This is only included for actionButtonTapped events.
The clipSecondaryActionsText (List<String>?) is an array of all the text CTAs on secondary actions for the clip. This can be included in Clip analytics events when the clip has secondary actions.
The clipSecondaryActionUrls (List<String>?) is an array of all the URLs on secondary actions for the clip. This can be included in Clip analytics events when the clip has secondary actions.
The clipSecondaryActionTypes (List<String>?) is an array of all the types of secondary actions for the clip. This can be included in Clip analytics events when the clip has secondary actions.
The followableCategoryLimitDialogue (String?) is the text displayed in the Followable Category Limit dialogue box. This is included for followableCategoryLimitShown, followableCategoryLimitActionButtonTapped, and followableCategoryLimitDismissed events.
The followableCategoryLimitActionText (String?) is the text displayed on the customisable action button in the Followable Category Limit dialogue box. This is included for followableCategoryLimitShown, followableCategoryLimitActionButtonTapped, and followableCategoryLimitDismissed events.
The followableCategoryLimitActionUrl (String?) is the URL associated with the customisable action button in the Followable Category Limit dialogue box. This is included for followableCategoryLimitShown, followableCategoryLimitActionButtonTapped, and followableCategoryLimitDismissed events.
The metadata (Map<String, String>?) contains custom metadata associated with the content for which the event occurred. For Clip events, this contains the metadata from the associated clip. For Story events, this contains the metadata from the associated page. It may be absent when content has no metadata.
Here's a complete example showing how to implement comprehensive analytics tracking:
import'package:storyteller_sdk/storyteller_sdk.dart';import'dart:async';classStorytellerAnalytics{StreamSubscription<StorytellerUserActivityEvent>?_analyticsSubscription;voidinitialize(){if(_analyticsSubscription!=null)return;_analyticsSubscription=Storyteller.onUserActivityOccurred.listen((event){// Log all eventsprint('Storyteller Event: ${event.type}');// Handle specific event typesswitch(event.type){case'openedStory':_handleStoryOpened(event.data);break;case'openedClip':_handleClipOpened(event.data);break;case'completedPage':_handlePageCompleted(event.data);break;case'likedClip':_handleClipLiked(event.data);break;// Add more cases as needed}// Forward to your analytics platform_forwardToAnalytics(event);},onError:(error){print('Analytics error: $error');},);}void_handleStoryOpened(StorytellerUserActivityDatadata){print('Story Opened: ${data.storyTitle}');print('Read Status: ${data.storyReadStatus}');print('Categories: ${data.categories}');}void_handleClipOpened(StorytellerUserActivityDatadata){print('Clip Opened: ${data.clipTitle}');print('Collection: ${data.collection}');print('Is Live: ${data.isLive}');}void_handlePageCompleted(StorytellerUserActivityDatadata){print('Page Completed: ${data.pageTitle}');print('Duration: ${data.contentLength}s');}void_handleClipLiked(StorytellerUserActivityDatadata){print('Clip Liked: ${data.clipTitle}');}void_forwardToAnalytics(StorytellerUserActivityEventevent){// Forward to Firebase, Segment, Mixpanel, etc.// analytics.track(event.type, {// 'storyId': event.data.storyId,// 'clipId': event.data.clipId,// ...event.data.fields,// // ... other relevant fields// });}Future<void>dispose()async{await_analyticsSubscription?.cancel();_analyticsSubscription=null;}}
Usage:
import'package:flutter/widgets.dart';Future<void>main()async{WidgetsFlutterBinding.ensureInitialized();// Subscribe before initialization/content interactions.finalanalytics=StorytellerAnalytics();analytics.initialize();finalresult=awaitStoryteller.initialize('your-api-key');if(!result.success){awaitanalytics.dispose();throwStateError(result.message??'Storyteller initialization failed');}runApp(MyApp());}
Open a known published Story/Clip and log the event type and raw payload. A
successful interaction produces openedStory or openedClip; closing the
player produces its dismissal event. Retain one listener per owning service
and cancel it during disposal to avoid duplicate forwarding.
If no events arrive, check that initialization succeeded, the listener was
attached before content interactions, and enableUserActivityTracking is
true. If IDs or titles are null, inspect await Storyteller.eventTrackingOptions().
Passing StorytellerEventTrackingOptions() sets enableFullVideoAnalytics to
false by default and native code filters content identifiers/titles. Omitting
the options argument to initialize currently defaults this flag to true on
both platform bridges. Set it explicitly according to your tracking policy;
see Privacy and Tracking.
Customer callbacks, Storyteller ingestion, and ad-provider tracking are
separate destinations. Receiving an event does not prove that it was ingested
or that an ad filled. For more diagnostic steps, see
Troubleshooting.
{"slug": "analytics", "page_title": "Analytics", "page_url": "Analytics/", "canonical_url": "/flutter/Analytics/", "markdown": "# Analytics\n\n## Table of Contents\n\n1. [Overview](#overview)\n1. [Listening to Events](#listening-to-events)\n1. [Event Types](#event-types)\n1. [Cross-cutting Events](#cross-cutting-events)\n1. [Story Events](#story-events)\n1. [Clip Events](#clip-events)\n1. [Card Events](#card-events)\n1. [Ad Events](#ad-events)\n1. [Organic Watch-time Events](#organic-watch-time-events)\n1. [Playback Events](#playback-events)\n1. [Sheet Events](#sheet-events)\n1. [Search Events](#search-events)\n1. [Event Data](#event-data)\n\n## Overview\n\nThe Storyteller SDK fires analytics events for various user interactions. You can subscribe to these events to track user engagement, forward analytics to your own systems, or implement custom behavior based on user actions.\n\n## Listening to Events\n\nSubscribe before mounting Storyteller content, retain the subscription, and\ncancel it when its owning service or widget is disposed. Events are live; the\nstream does not replay interactions that occurred before the listener.\n\nTo receive analytics events, subscribe to the `onUserActivityOccurred` stream provided by the SDK:\n\n```dart\nimport 'package:storyteller_sdk/storyteller_sdk.dart';\n\n// Subscribe to all user activity events\nStoryteller.onUserActivityOccurred.listen((event) {\n print('Event Type: ${event.type}');\n print('Story ID: ${event.data.storyId}');\n print('Story Title: ${event.data.storyTitle}');\n\n // Forward to your analytics system\n // Pass a map to your analytics provider, for example:\n // analytics.track(event.type, event.data.fields);\n});\n```\n\nEach event is a `StorytellerUserActivityEvent` containing:\n\n- `type` - The event type as a String (e.g., `'openedStory'`, `'completedPage'`)\n- `data` - A `StorytellerUserActivityData` object with detailed information about the event\n\n`type` is the unmodified native string, and `data.fields` contains the complete\npayload delivered to Dart by the platform bridge. Typed Dart properties are\nconveniences for known fields; preserve the raw values when forwarding\nanalytics so delivered keys without a typed Flutter property remain usable.\n\nFor more information on implementing event streams, see the [Storyteller Delegate](StorytellerDelegate.md) page.\n\nNeed a production-ready listener example? The Showcase service wires this stream (and others) in [`StorytellerService._setupEventListeners`](https://github.com/getstoryteller/storyteller-showcase-flutter/blob/main/lib/services/storyteller_service.dart#L244).\n\n## Event Types\n\nUse the exact native wire strings below when comparing `event.type`; Flutter\ndoes not rename them. For example, the Opened Story heading corresponds to\n`openedStory`. Shared action/share event names apply to Stories and Clips; use\nthe delivered Story/Clip fields to identify their context.\n\nThis guide describes Flutter integration behavior. The governed cross-platform\nAnalytics reference is maintained separately and its publication is independent\nof an SDK release. Event availability depends on the package/native versions;\nsee the [Changelog](Changelog.md) before adopting Unreleased events.\n\nIn the discussion below, completing a Page means allowing its playback/timer\nto finish: the configured duration for an image Page or the playback duration\nfor a video. Do not assume a fixed 15-second image duration.\n\n## Cross-cutting Events\n\n### Tile Visible\n\n**Event Type:** `tileVisible`\n\nThis event is emitted when a Story or Clip list tile becomes visible through an\ninitial render, scrolling, or a true list reload/data refresh. `tileIndex` is\nthe one-based visible tile position. The optional `context` map carries the\nintegrator context associated with the list. Android may additionally supply\n`originalPosition` and `displayPosition` for reordered lists.\n\n```dart\nStoryteller.onUserActivityOccurred.listen((event) {\n if (event.type == 'tileVisible') {\n print('Tile ${event.data.tileIndex}: ${event.data.context}');\n }\n});\n```\n\n### SDK Initialized\n\n`sdkInitialized` is internal Storyteller telemetry. Android and iOS exclude it\nfrom customer analytics callbacks, so it does not arrive through\n`onUserActivityOccurred`. Use the result returned by\n`await Storyteller.initialize(...)` to check initialization success. The\n[SDK Initialization Fields](#sdk-initialization-fields) retained on the Dart\ndata type do not change this delivery rule.\n\n### Unsupported and Future Events\n\nAndroid declares `unsupported`, and the native SDK may emit a newer event\nname that is not listed\non this page. Flutter deliberately retains that string in `event.type`, so no\nDart enum update is required to receive it. Every key delivered by the platform\nbridge is retained in `event.data.fields`. iOS's Codable bridge naturally\nincludes newly encoded keys; Android native payload additions still require the\nFlutter Android mapper to be updated during the corresponding native SDK bump.\n\n## Story Events\n\nThe following properties may be included in Story-related events:\n\n- `airplayEnabled`\n- `captionsEnabled`\n- `contentLength`\n- `currentCategory`\n- `eyebrow`\n- `metadata`\n- `pageActionText`\n- `pageActionType`\n- `pageActionUrl`\n- `actionLinkId`\n- `pageHasAction`\n- `pageId`\n- `pageIndex`\n- `pageTitle`\n- `pageType`\n- `storyDisplayTitle`\n- `storyId`\n- `storyPageCount`\n- `storyPlaybackMode`\n- `storyTitle`\n\n### Opened Story\n\n**Event Type:** `openedStory`\n\nThis event is recorded in the following scenarios:\n\n- When a user taps on a row item to open a story\n- When a story is loaded because the previous Story finished\n- When a story is loaded because the user tapped to skip the last page of the previous story\n- When a user swipes left on a story to go to the next story\n- When a user swipes right on a story to go to the previous story\n- When a user is sent directly to a story via a deep link\n\nWhenever an Opened Story event occurs, additional event data specific to this event includes `storyReadStatus`, `categories`, `categoryDetails`, `storyIndex`, and `openedReason`.\n\n**Example:**\n```dart\nStoryteller.onUserActivityOccurred.listen((event) {\n if (event.type == 'openedStory') {\n print('Story opened: ${event.data.storyTitle}');\n print('Read status: ${event.data.storyReadStatus}');\n print('Opened reason: ${event.data.openedReason}');\n }\n});\n```\n\n### Opened Page\n\n**Event Type:** `openedPage`\n\nThis event is recorded whenever a user views content from a page.\n\n`openedPage` identifies a Page being opened, including image and interactive\nPages. It does not by itself prove that video playback started; use\n`mediaStarted` for the native playback-start signal.\n\nWhenever an Opened Page event occurs, additional event data specific to this event includes `openedReason`.\n\n### Dismissed Story\n\n**Event Type:** `dismissedStory`\n\nThis event is recorded in the following scenarios:\n\n- When a user taps the close button to dismiss the story\n- When a user swipes down to dismiss the story\n- When a user taps to skip the last page of the final story - this dismisses the story and exits the story view\n- When a user swipes left on the final story to dismiss the story\n- When a user swipes right on the first story to dismiss the story\n- When a user completes the final page of the final story and the story view is dismissed\n\nWhenever a Dismissed Story event occurs, additional event data specific to this event includes `dismissedReason`, `durationViewed`, and `pagesViewedCount`.\n\n### Skipped Story\n\n**Event Type:** `skippedStory`\n\nThis event is recorded when:\n\n- a user swipes left to go to the next story\n- a user skips the last page of a story to go to the next story\n\n### Skipped Page\n\n**Event Type:** `skippedPage`\n\nThis event is recorded when a user taps to go to the next page before completing the current page.\n\n### Completed Story\n\n**Event Type:** `completedStory`\n\nThis event is recorded at the same time as `openedPage` for the final page in a story.\n\n### Completed Page\n\n**Event Type:** `completedPage`\n\nThis event is recorded when a user watches a page to completion (i.e. the timer for that page finishes.)\n\n### Action Button Tapped\n\n**Event Type:** `actionButtonTapped`\n\nThis event is recorded when a user taps an action button on a page to open a link.\n\nWhenever an Action Button Tapped event occurs, additional event data specific to this event includes `actionText`.\n\n### Share Button Tapped\n\n**Event Type:** `shareButtonTapped`\n\nThis event is recorded when a user taps the share button on a page.\n\nWhenever a Share Button Tapped event occurs, additional event data specific to this event includes `shareMethod`.\n\n### Previous Story\n\n**Event Type:** `previousStory`\n\nThis event is recorded when:\n\n- a user swipes right to go to the previous story (unless this is the first story - in which case `dismissedStory` is fired instead)\n- a user taps back on the first page in a story (and this is not the first page in the first story)\n\n### Previous Page\n\n**Event Type:** `previousPage`\n\nThis event is recorded when a user taps back to go to a previous page in the story.\n\n### Share Success\n\n**Event Type:** `shareSuccess`\n\nThis event is recorded when a user selects a sharing method from the system dialog.\n\nWhenever a Share Success event occurs, additional event data specific to this event includes `shareMethod`.\n\n### Voted Poll\n\n**Event Type:** `votedPoll`\n\nThis event is recorded when user votes in a poll.\n\nWhenever a Voted Poll event occurs, additional event data specific to this event includes `pollAnswerId`.\n\n### Trivia Quiz Question Answered\n\n**Event Type:** `triviaQuizQuestionAnswered`\n\nThis event is recorded when a user answers a question in a trivia quiz.\n\nWhenever a Trivia Quiz Question Answered event occurs, additional event data specific to this event includes `triviaQuizId`, `triviaQuizTitle`, `triviaQuizQuestionId`, and `triviaQuizAnswerId`.\n\n### Trivia Quiz Completed\n\n**Event Type:** `triviaQuizCompleted`\n\nThis event is recorded when a user completes a trivia quiz.\n\nWhenever a Trivia Quiz Completed event occurs, additional event data specific to this event includes `triviaQuizId`, `triviaQuizTitle`, and `triviaQuizScore`.\n\n### Story Instructions Screen Viewed\n\n**Event Type:** `storyInstructionsScreenViewed` (iOS),\n`storyInstructionScreenViewed` (Android)\n\nThis event is recorded when:\n\n- The Story instruction screen appears to users\n\nWhenever a Story Instructions Screen Viewed event occurs, additional event data specific to this event includes `storyReadStatus` and `openedReason`.\n\n## Clip Events\n\nThe following properties may be included in Clip-related events:\n\n- `airplayEnabled`\n- `captionsEnabled`\n- `categories`\n- `categoryDetails`\n- `clipActionText`\n- `clipActionType`\n- `clipActionUrl`\n- `actionLinkId`\n- `collectionClipCount`\n- `clipFeedType`\n- `clipHasAction`\n- `clipHasSecondaryActions`\n- `clipId`\n- `clipIndex`\n- `clipSecondaryActionTypes`\n- `clipSecondaryActionUrls`\n- `clipSecondaryActionsText`\n- `clipTitle`\n- `contentLength`\n- `isLive`\n- `metadata`\n\nThe following additional properties may be included when available:\n\n- `eyebrow`\n- `collection`\n- `collectionTitle`\n- `categoryId`\n- `categoryName`\n\n### Opened Clip\n\n**Event Type:** `openedClip`\n\nThis event is recorded when:\n\n- a user taps on a row or grid item to open a clip\n- a user swipes up to the next clip\n- a user swipes down to the previous clip\n- a user is sent directly to a clip via a call to `openCollection`\n- a user is sent directly to a clip via a deep link\n- a user opens a Category and navigates to a new Clip or moves back to a previous Category\n- a user dismisses the last Category (by pressing back or swiping right) and returns to the top-level Collection\n- a user opens embedded Clips for the first time\n- a user pulls to refresh at the top of Embedded Clips\n- a user is viewing a Collection with \"For You/Following\" enabled and switches between \"For You\" and \"Following\"\n\nWhenever an Opened Clip event occurs, additional event data specific to this event includes `openedReason`.\n\n**Example:**\n```dart\nStoryteller.onUserActivityOccurred.listen((event) {\n if (event.type == 'openedClip') {\n print('Clip opened: ${event.data.clipTitle}');\n print('Clip ID: ${event.data.clipId}');\n print('Collection: ${event.data.collection}');\n }\n});\n```\n\n### Dismissed Clip\n\n**Event Type:** `dismissedClip`\n\nThis event is recorded when:\n\n- a user taps on the back button in the top-left to exit the Clips player (and is at the top of the stack of Clip Categories)\n\nThis event does not fire when the user is not at the top of the Clip Category stack.\nWhenever a Dismissed Clip event occurs, additional event data specific to this event includes `dismissedReason`, `durationViewed`, `clipsViewed`, and `loopsViewed`.\n\n### Next Clip\n\n**Event Type:** `nextClip`\n\nThis event is recorded when:\n\n- a user swipes up to go to the next clip\n\n### Previous Clip\n\n**Event Type:** `previousClip`\n\nThis event is recorded when:\n\n- a user swipes down to go to the previous clip\n\n### Completed Loop\n\n**Event Type:** `completedLoop`\n\nThis event is recorded when:\n\n- a user completes a loop of a clip\n\nWhenever a Completed Loop event occurs, additional event data specific to this event includes `completionType`.\n\n> Note: For live clips this event will never be fired.\n\n### Action Button Tapped\n\n**Event Type:** `actionButtonTapped`\n\nThis event is recorded when:\n\n- a user taps the primary action button at the bottom of a Clip\n- a user taps any secondary action buttons above the clip title\n- a user swipes left on a clip to open the relevant action\n\nWhenever an Action Button Tapped event occurs, additional event data specific to this event includes `actionText`, `actionClass`, `actionIndex`, `tappedClipActionText`, `tappedClipActionUrl`, and `tappedClipActionType`.\n\n### Share Button Tapped\n\n**Event Type:** `shareButtonTapped`\n\nThis event is recorded when:\n\n- a user taps the share button on a clip\n\n### Share Success\n\n**Event Type:** `shareSuccess`\n\nThis event is recorded when:\n\n- a user selects and successfully shares a page from the system dialog\n\nWhenever a Share Success event occurs, additional event data specific to this event includes `shareMethod`.\n\n### Paused Clip\n\n**Event Type:** `pausedClip`\n\nThis event is recorded when:\n\n- a user taps on the screen whilst a clip is playing to pause the clip - it does not fire when a clip is paused automatically by sharing or following an action\n\n> Note: For live clips this event will never be fired, as pausing/resuming live clips is not supported.\n\n### Resumed Clip\n\n**Event Type:** `resumedClip`\n\nThis event is recorded when:\n\n- a user taps a clip that is paused to resume playback - it does not fire when a clip is resumed automatically\n\n> Note: For live clips this event will never be fired, as pausing/resuming live clips is not supported.\n\n### Scrubbed Clip\n\n**Event Type:** `scrubbedClip`\n\nThis event is recorded when:\n\n- a user scrubs the clip to a different position by dragging the clip progress bar\n\nWhenever a Scrubbed Clip event occurs, additional event data specific to this event includes `openedReason`, `startPosition`, `endPosition`, and `scrubDirection`.\n\n### Liked Clip\n\n**Event Type:** `likedClip`\n\nThis event is recorded when:\n\n- a user likes a clip by tapping the like button when they do not currently like the clip\n\n### Unliked Clip\n\n**Event Type:** `unlikedClip`\n\nThis event is recorded when:\n\n- a user unlikes a clip by tapping the like button when they currently like the clip\n\n### Finished Clip\n\n**Event Type:** `finishedClip`\n\nThis event is recorded when:\n\n- Dismissed Clip is fired\n- Next Clip is fired\n- Previous Clip is fired\n- Opened Category is fired\n- The clip reaches the end and starts a new loop\n- A user is viewing a Collection with \"For You/Following\" enabled and switches between \"For You\" and \"Following\"\n\nWhenever a Finished Clip event occurs, additional event data specific to this event includes `loopsViewed`, `durationViewed`, and `completionType`.\n\n### Opened Category\n\n**Event Type:** `openedCategory`\n\nThis event is recorded when:\n\n- a user taps a category at the bottom of the Clips Player to open a new Category\n- a user opens a category by navigating back from another category\n\n> Note: This event is not fired when opening the top level of a collection - only when navigating to a specific category.\n\n### Dismissed Category\n\n**Event Type:** `dismissedCategory`\n\nThis event is recorded when:\n\n- a user navigates back using the back button to the previous category\n\nThis event does not fire when dismissing the top level collection.\n\n### Follow Category\n\n**Event Type:** `followCategory`\n\nThis event is recorded when:\n\n- a user taps the plus button under the follow category button of the Clips Player\n- a user taps the follow button at the top right of the Followable Category screen and the category was not followed\n\n### Unfollow Category\n\n**Event Type:** `unfollowCategory`\n\nThis event is recorded when:\n\n- a user taps the checkmark button under the follow category button of the Clips Player\n- a user taps the follow button at the top right of the Followable Category screen and the category was followed\n\n### Followable Category Tapped\n\n**Event Type:** `followableCategoryTapped`\n\nThis event is recorded when:\n\n- a user taps the follow category button of the Clips Player to open the Followable Category screen\n- a user swipes left from the right edge of the screen on a Clip with a Followable Category and opens the Followable Category screen\n\n### Enable Captions\n\n**Event Type:** `enabledClipCaptions`\n\nThis event is recorded when:\n\n- a user taps the button to enable Closed Captions in the Clips Player\n\n### Disable Captions\n\n**Event Type:** `disabledClipCaptions`\n\nThis event is recorded when:\n\n- a user taps the button to disable Closed Captions in the Clips Player\n\n### Followable Category Limit Shown\n\n**Event Type:** `followableCategoryLimitShown`\n\nThis event is recorded when:\n\n- the alert dialogue box associated with the Followable Category Limit is shown\n\nWhenever a Followable Category Limit Shown event occurs, additional event data specific to this event includes `followableCategoryLimitDialogue`, `followableCategoryLimitActionText`, and `followableCategoryLimitActionUrl`.\n\n### Followable Category Limit Action Button Tapped\n\n**Event Type:** `followableCategoryLimitActionButtonTapped`\n\nThis event is recorded when:\n\n- a user taps on the customisable action button in the Followable Category Limit Dialogue Box\n\nWhenever a Followable Category Limit Action Button Tapped event occurs, additional event data specific to this event includes `followableCategoryLimitDialogue`, `followableCategoryLimitActionText`, and `followableCategoryLimitActionUrl`.\n\n### Followable Category Limit Dismissed\n\n**Event Type:** `followableCategoryLimitDismissed`\n\nThis event is recorded when:\n\n- a user dismisses the Followable Category Limit Dialogue Box by tapping the cancel action button\n\nWhenever a Followable Category Limit Dismissed event occurs, additional event data specific to this event includes `followableCategoryLimitDialogue`, `followableCategoryLimitActionText`, and `followableCategoryLimitActionUrl`.\n\n## Card Events\n\nThe following properties may be included in Card-related events:\n\n- `cardActionType`\n- `cardActionUrl`\n- `cardAspectRatio`\n- `cardBackgroundType`\n- `cardCollectionId`\n- `cardId`\n- `cardIndex`\n- `cardSubtitle`\n- `cardTitle`\n- `categories`\n- `categoryDetails`\n- `hasButton`\n- `isLive`\n- `actionLinkId`\n\n### Card Viewed\n\n**Event Type:** `cardViewed`\n\nThis event is recorded when:\n\n- a Storyteller Card appears on screen, either by scrolling to it or opening a view where its visible\n\n### Card Tapped\n\n**Event Type:** `cardTapped`\n\nThis event is recorded when:\n\n- a user taps on the Storyteller Card view\n\n### Card Media and Audio\n\n**Event Types:** `cardVideoStarted`, `cardVideoHeartbeat`,\n`cardVideoCompleted`, `cardAudioToggled`\n\nVideo Cards emit playback-start, active-watch-time heartbeat, and completion\nevents. `durationViewed` is milliseconds; `videoStartReason` identifies the\nstart reason when available. Audio state changes supply `audioToggleFrom`,\n`audioToggleTo`, and `isMuted`. Image Cards do not emit video playback events.\nThese events are also available in Flutter 11.6.5.\n\n## Ad Events\n\nThe following properties may be included in Ad-related events:\n\n- `actionText`\n- `actionType`\n- `actionUrl`\n- `adFormat`\n- `adId`\n- `adIndex`\n- `adPlacement`\n- `adRequestId`\n- `adResponseIdentifier`\n- `adSlotType`\n- `adUnitId`\n- `adStrategy`\n- `adType`\n- `advertiserName`\n- `airplayEnabled`\n- `categories`\n- `categoryDetails`\n- `contentLength`\n- `currentCategory`\n- `hasAction`\n- paid ad fields such as `adValueMicros`, `adCurrencyCode`, and mediation\n source metadata when provided by the native SDK\n\nFor Story Ad events specifically, additional common fields include:\n\n- `pageActionText`\n- `pageActionType`\n- `pageActionUrl`\n- `pageHasAction`\n- `pageType`\n- `storyPlaybackMode`\n\nFor Clip Ad events specifically, additional common fields include:\n\n- `clipFeedType`\n- `clipIndex`\n- `collection`\n- `collectionClipCount`\n- `loopsViewed`\n\n### Opened Ad\n\n**Event Type:** `openedAd`\n\n#### Stories\n\nThis event is recorded when:\n\n- an ad is loaded because the previous story finished\n- a user swipes left on a story to go to the next story and an ad appears\n- a user swipes right on a story to go to the previous story and an ad appears\n\nWhenever an Opened Ad event occurs, additional event data specific to this event includes `openedReason`.\n\n#### Clips\n\nThis event is recorded when:\n\n- a user swipes up on a clip to go to the next clip and an ad should appear next\n- a user swipes down on a clip to go to the previous clip and an ad should appear next\n- an ad completes a loop and begins playing again from the start\n\n### Dismissed Ad\n\n**Event Type:** `dismissedAd`\n\n#### Stories\n\nThis event is recorded when a user:\n\n- taps close to dismiss the ad\n- swipes down to dismiss the ad\n- taps back on their device UI to dismiss the ad (Android only)\n- taps to skip the ad if the ad is the last page in the current set of stories\n- swipes left on the ad to skip it if the ad is the last page in the current set of stories\n- completes the ad if the ad is the last page in the current set of stories\n\nWhenever a Dismissed Ad event occurs, additional event data specific to this event includes `durationViewed`, `dismissedReason` and `pagesViewedCount`.\n\n#### Clips\n\nThis event is recorded when a user:\n\n- taps the back button to exit the clips player when an ad is being shown\n\nWhenever a Dismissed Ad event occurs, additional event data specific to this event includes `durationViewed`, and `clipsViewed`.\n\n### Skipped Ad\n\n**Event Type:** `skippedAd`\n\n#### Stories\n\nThis event is recorded when a user:\n\n- swipes left to go to the next story before completing the current ad\n- taps to go to the next page on an ad before completing the current ad\n\n#### Clips\n\nThis event is recorded when:\n\n- a user swipes up to go to the next clip\n- a user swipes down to go the previous clip\n\n### Ad Action Button Tapped\n\n**Event Type:** `adActionButtonTapped`\n\n#### Stories\n\nThis event is recorded when a user:\n\n- swipes up on an ad to open a link\n- taps on the swipe up element of an ad to open a link\n\n#### Clips\n\nThis event is recorded when a user:\n\n- taps the action button at the bottom of an ad displayed in clips\n- swipes left on a clip to open the relevant action\n\n### Finished Ad\n\n**Event Type:** `finishedAd`\n\n#### Stories\n\nThis event is recorded at the same time as Dismissed Ad, Skipped Ad and Viewed Ad Page Complete and gives an easier way to determine when an ad finishes for any reason.\n\n#### Clips\n\nThis event is recorded at the same time as DismissedAd, or SkippedAd.\n\n### Paused Ad\n\n**Event Type:** `pausedAdPage`\n\n#### Stories\n\nThis event is recorded when a user pauses a page within an ad by pressing and holding on the page.\n\n#### Clips\n\nThis event is recorded when a user pauses a clip which is an ad by tapping the screen. It does not fire when a clip is paused automatically for sharing or following an action.\n\n### Resumed Ad\n\n**Event Type:** `resumedAdPage`\n\n#### Stories\n\nThis event is recorded when a user resumes playing a page within an ad by releasing their long press which paused the ad.\n\n#### Clips\n\nThis event is fired when a clip which is an ad is paused and a user taps the screen to resume playback. It does not fire when a clip is resumed automatically.\n\n### Viewed Ad Page First Quartile\n\n**Event Type:** `viewedAdPageFirstQuartile`\n\nThis event is recorded when a user reaches 1/4 of the way through the duration of a page or clip which is an ad.\n\nFor clip ads (which loop), it fires for each loop of the clip.\n\n### Viewed Ad Page Midpoint\n\n**Event Type:** `viewedAdPageMidpoint`\n\nThis event is recorded when a user reaches halfway through the duration of a page or clip which is an ad.\n\nFor clip ads (which loop), it fires for each loop of the clip.\n\n### Viewed Ad Page Third Quartile\n\n**Event Type:** `viewedAdPageThirdQuartile`\n\nThis event is recorded when a user reaches 3/4 of the way through the duration of a page or clip which is an ad.\n\nFor clip ads (which loop), it fires for each loop of the clip.\n\n### Viewed Ad Page Complete\n\n**Event Type:** `viewedAdPageComplete`\n\nThis event is recorded when a user reaches the end of the duration for a page or clip which is an ad.\n\nFor clip ads (which loop), it fires for each loop of the clip.\n\n### Ad Request Lifecycle and Revenue\n\n**Event Types:** `adOpportunity`, `adRequested`, `adLoaded`,\n`adFailedToLoad`, `adPaid`\n\nThese native events describe an eligible ad opportunity, request, successful\nload, load failure, or paid impression. They depend on the configured ad\nintegration and provider; an opportunity does not guarantee a request, fill,\nor revenue. Correlate delivered `playerSessionId`, `adRequestId`, `adSlotType`,\n`adUnitId`, source/error fields, and paid value/currency fields when present.\n\n### Ad Session Summary\n\n**Event Type:** `adSessionSummary`\n\nThis event summarises the ad session after the native SDK has completed its session-level accounting. When present, the payload can include counts for opportunities, requests, loads, failed loads, paid events, and total revenue micros.\n\n## Organic Watch-time Events\n\n**Event Types:** `storyHeartbeat`, `storyPageQuartile`,\n`clipVideoHeartbeat`, `clipVideoQuartile`\n\nFlutter 12.1.2 delivers these events with Android 12.2.0 and iOS 11.9.0.\nThey are absent from Flutter 11.6.5. Story\nheartbeats describe the current non-ad Page; Clip heartbeats describe the\ncurrent Clip. Heartbeats report accumulated active watch time in milliseconds\nthrough `durationViewed`, at five-second milestones. Paused or inactive\nplayback does not advance active watch time.\n\nQuartile events report playback progress milestones of 25, 50, and 75 through\n`event.data.quartile`.\nQuartiles measure content progress and can be crossed by scrubbing, so they\nare different from elapsed watch time. Use `completedPage`/`completedLoop`\nfor completion and the ad quartile events for ads.\n\n```dart\nStoryteller.onUserActivityOccurred.listen((event) {\n if (event.type == 'storyHeartbeat' || event.type == 'clipVideoHeartbeat') {\n print('Active watch time: ${event.data.durationViewed} ms');\n } else if (event.type == 'storyPageQuartile' ||\n event.type == 'clipVideoQuartile') {\n final quartile = event.data.quartile;\n print('Playback progress: $quartile%');\n }\n});\n```\n\n## Playback Events\n\nThe following properties may be included in Playback-related events:\n\n- `pageActionText`\n- `pageActionUrl`\n- `pageHasAction`\n- `pageId`\n- `pageIndex`\n- `pageTitle`\n- `pageType`\n- `storyDisplayTitle`\n- `storyId`\n- `storyIndex`\n- `storyPlaybackMode`\n- `storyTitle`\n- `videoStartReason`\n\n### Ready to Play\n\n**Event Type:** `readyToPlay`\n\nThis event is called once per video page at the point when the video player has been loaded.\n\n### Media Started\n\n**Event Type:** `mediaStarted`\n\nThis event is called once per video page at the point when the video starts to play for the first time.\n\n### Buffering Started\n\n**Event Type:** `bufferingStarted`\n\nThis event is called on video pages whenever the video starts to buffer.\n\nWhenever a Buffering Started event occurs, additional event data specific to this event includes `isInitialBuffering`.\n\n### Buffering Ended\n\n**Event Type:** `bufferingEnded`\n\nThis event is called on video pages whenever the video finishes buffering.\n\n> Note: There should be at most one Ready to Play event and one Media Started event for every page. There may be multiple Buffering Started/Buffering Ended pairs of events for an individual page. There may not always be a Buffering Ended event for every Buffering Started event as the user may choose to exit the page during buffering.\n\nWhenever a Buffering Ended event occurs, additional event data specific to this event includes `isInitialBuffering` and `timeSinceBufferingBegan`.\n\n### Audio and Story Captions\n\n**Event Types:** `mutedStory`, `unmutedStory`, `mutedClip`, `unmutedClip`,\n`mutedAd`, `unmutedAd`, `enabledStoryCaptions`, `disabledStoryCaptions`\n\nThese events represent native mute or Story-caption state changes. Clip\ncaption changes use `enabledClipCaptions` and `disabledClipCaptions` above.\nUse nullable `isMuted`/`captionsEnabled` for state when supplied.\n\n### Current Video Player Changed\n\n**Event Type:** `onCurrentVideoPlayerChanged`\n\nThe native player reports a change of its current video-player instance.\nThis technical callback is distinct from opening or completing content.\n\n## Sheet Events\n\nThe following properties may be included in Sheet-related events:\n\n- `actionText`\n- `captionsEnabled`\n- `categories`\n- `categoryDetails`\n- `clipActionText`\n- `clipActionType`\n- `clipActionUrl`\n- `clipFeedType`\n- `clipHasAction`\n- `clipId`\n- `clipIndex`\n- `clipTitle`\n- `collection`\n- `collectionClipCount`\n- `containerTitle`\n- `currentCategory`\n- `isLive`\n- `pageActionText`\n- `pageActionType`\n- `pageActionUrl`\n- `pageHasAction`\n- `pageId`\n- `pageIndex`\n- `pageTitle`\n- `pageType`\n- `searchFilter`\n- `searchSort`\n- `searchTerm`\n- `sheetId`\n- `sheetSize`\n- `sheetTitle`\n- `storyId`\n- `storyIndex`\n- `storyPageCount`\n- `storyTitle`\n\n### Opened Sheet\n\n**Event Type:** `openedSheet`\n\nThis event is recorded when a user opens a Sheet.\n\nWhenever an Opened Sheet event occurs, additional event data specific to this event includes `openedReason`.\n\n### Dismissed Sheet\n\n**Event Type:** `dismissedSheet`\n\nThis event is recorded when a user closes a Sheet.\n\n## Search Events\n\nThe following properties may be included in Search-related events:\n\n- `categories`\n- `categoryDetails`\n- `currentCategory`\n- `isSuggestion`\n- `searchFilter`\n- `searchFrom`\n- `searchSort`\n- `searchTerm`\n\nWhen search is opened from a Story context, additional properties include:\n\n- `pageActionText`\n- `pageActionUrl`\n- `pageHasAction`\n- `pageId`\n- `pageIndex`\n- `pageTitle`\n- `pageType`\n- `storyDisplayTitle`\n- `storyId`\n- `storyIndex`\n- `storyPageCount`\n- `storyPlaybackMode`\n- `storyReadStatus`\n- `storyTitle`\n\nWhen search is opened from a Clip context, additional properties include:\n\n- `clipActionText`\n- `clipActionUrl`\n- `clipHasAction`\n- `clipId`\n- `clipIndex`\n- `clipTitle`\n- `collection`\n\n### Opened Search\n\n**Event Type:** `openedSearch`\n\nThe openedSearch event is recorded when:\n\n- A user taps on Search from a Story within the Story player\n- A user taps on Search from a Clip within the Clip player\n- The `Storyteller.openSearch()` function is called\n\n### Dismissed Search\n\n**Event Type:** `dismissedSearch`\n\nThe dismissedSearch event is recorded when a user taps the 'X' button to exit the Search interface.\n\nWhenever an Dismissed Search event occurs, additional event data specific to this event includes `dismissedReason`.\n\n### Performed Search\n\n**Event Type:** `performedSearch`\n\nThe performedSearch event is recorded when:\n\n- A user taps the 'Search' icon after entering a term in the Search bar (whether manually or by tapping the 'arrow' icon beside a Search suggestion to populate the search bar)\n- A user taps the 'Search' icon beside a Search suggestion. The Search is then performed with the suggestion as the term.\n- A user taps 'Apply filters' from the filters interface.\n\n### Opened Filters\n\n**Event Type:** `openedFilters`\n\nThe openedFilters event is recorded when, after a user has performed as Search, they press the 'Filter' icon to bring up the filter interface.\n\n> Note: That filters can only be applied after the initial search has been performed.\n\n### Dismissed Filters\n\n**Event Type:** `dismissedFilters`\n\nThis event is recorded when a user, after calling up the 'Filters' interface, swipes down to exit the interface without applying any.\n\n### Used Suggestion\n\n**Event Type:** `usedSuggestion`\n\nThe usedSuggestion event is recorded when:\n\n- A user taps the 'arrow' icon beside a Search suggestion to populate the Search bar. This does not trigger any other event, and the user may amend the Search bar input before performing a Search.\n- A user taps the 'Search' icon beside a Search suggestion. This simultaneously triggers a PerformedSearch event, above.\n\nWhenever a Used Suggestion event occurs, additional event data specific to this event includes `initialInput`.\n\n### External Search Result Tapped\n\n**Event Type:** `searchResultTapped`\n\nA tap on a tenant-configured external Search result can supply `actionType`,\n`searchResultId`, `searchResultTitle`, and `searchResultSource`, available as\ntyped `StorytellerUserActivityData` properties from Flutter 12.1.2.\nExternal result visibility uses `tileVisible` when applicable.\n\n## Event Data\n\nEvent data is exposed as `StorytellerUserActivityData`. Its typed properties\nare nullable and depend on the event, content, platform, and tracking options.\nA list of fields on this page is not a guarantee that each one is populated.\nNumeric values are converted to the declared Dart `int` or `double`; string\nlists and the typed `metadata`/`context` maps convert entries to strings.\n`fields` preserves the original delivered values instead of those conversions.\n\n### Additional and Legacy Properties\n\n`categoryType`, `location`, `locale`, and `itemTitle` are nullable strings whose\npresence depends on the native event. `playerSessionId` (`String?`) can\ncorrelate player/ad activity. `videoStartReason` (`String?`) can be `natural`\nor `loop` for video Cards.\n\nThe Dart type also retains legacy `collectionId`, `collectionTitle`,\n`searchQuery`, `url`, `externalId`, `duration`, `position`, `error`,\n`errorMessage`, and `customData` properties. A retained getter does not\nguarantee that the current native bridge supplies that key. In particular,\n`externalId` is used by native Storyteller ingestion and is not forwarded as a\ncustomer analytics field by the current Android mapper.\n\n### Raw Fields\n\nThe `fields` (`Map<String, dynamic>`) property contains every string-keyed\npayload value delivered to Dart by the platform bridge, including values which\ndo not have a typed Dart property. Preserve this map when forwarding events if\nyour integration must remain forward compatible. On Android, a newly added\nnative field must first be added to the Flutter plugin's explicit mapper.\n\n### Tile and List Position\n\nThe `tileIndex` (`int?`) property is the one-based position for a\n`tileVisible` event. `originalPosition` and `displayPosition` (`int?`) are\nAndroid-only positions supplied for reordered lists when available.\n\n### Analytics Context\n\nThe `context` (`Map<String, String>?`) property contains integrator context\nprovided to a supported list, Card collection, or open call. Android delivers\nnonempty context to customer callbacks but removes it from Storyteller API\ningestion; iOS includes it in callback payloads and encoded activity records.\nDo not assume identical downstream persistence across platforms. An omitted\nor empty Android context is absent from the bridge payload.\n\n### SDK Initialization Fields\n\nThese typed fields describe internal `sdkInitialized` telemetry, which is\nexcluded from customer callbacks on Android and iOS. Their presence on the\nDart type does not make them initialization notifications. They include `screenResolution`, `appId`,\n`appName`, `deviceModel`, `appVersion`, `operatingSystem`, `osVersion`,\n`deviceType`, `deviceBrand`, `minOsVersion`, `initializationSucceeded`,\n`enablePersonalization`, `enableStorytellerTracking`,\n`enableUserActivityTracking`, `enableAdTracking`,\n`enableFullVideoAnalytics`, and `enableRemoteViewingStore`. Android may also\nprovide `targetOsVersion` and `languageVersion`.\n\n### Story ID\n\nThe `storyId` (`String?`) is the ID of the story for which the event occurred.\n\n### Story Title\n\nThe `storyTitle` (`String?`) is the title of the story for which the event occurred.\n\n### Story Display Title\n\nThe `storyDisplayTitle` (`String?`) is the display title of the Story for which the event occurred.\n\n### Story Index\n\nThe `storyIndex` (`int?`) is the position of the story in its source list when\nprovided. It can accompany Story events beyond `openedStory`.\n\n> Note: This value is 1-based.\n\n### Story Page Count\n\nThe `storyPageCount` (`int?`) is the number of pages in the story.\n\n### Story Read Status\n\nThe `storyReadStatus` (`String?`) describes the native read state when\nprovided, including on `openedStory` and some other Story events.\n\n> Note: This will either be `read` or `unread`.\n\n### Page ID\n\nThe `pageId` (`String?`) is the ID of the page for which the event occurred.\n\n### Page Index\n\nThe `pageIndex` (`int?`) is the index of the page in the story for which the event occurred.\n\n> Note: This value is 1-based.\n\n### Page Type\n\nThe `pageType` (`String?`) is the type of the page associated with the event. Native values include `image`, `video`, `poll`, `triviaQuiz`, and `empty`\nwhen applicable.\n\n### Story Playback Mode\n\nThe `storyPlaybackMode` (`String?`) value states if the story was opened during the list or in the single story mode (Storyteller static method.) When supplied, the values are `list` or `singleStory`.\n\n### Page Has Action\n\nThe `pageHasAction` (`bool?`) value states whether the page associated with the event contains an action.\n\n### Page Action Type\n\nThe `pageActionType` (`String?`) is the type of the action on the page.\n\n### Page Action Text\n\nThe `pageActionText` (`String?`) is the text call to action if the page has an action.\n\n### Page Action URL\n\nThe `pageActionUrl` (`String?`) is the URL for the link if the page has an action.\n\n### Action Link ID\n\nThe `actionLinkId` (`String?`) is the unique identifier of the action associated with the current story page, clip or card. This is not included for Ad events.\n\n### Opened Reason\n\nThe `openedReason` (`String?`) value states how the user opened a Story or Clip. The possible values for this are:\n- `storyListTap`: The user tapped the Story in the Story row.\n- `clipListTap`: The user tapped the Clip in a Clip list.\n- `deepLink`: Content was opened with deep-link attribution. The Dart API\n option is `StorytellerOpenReason.deeplink` (channel value `deeplink`), while\n the emitted analytics value is `deepLink`.\n- `swipe`: The user swiped left or right to change the current Story.\n- `automaticPlayback`: The user completed the previous Page.\n- `card`: The user taps on a Card.\n- `clipActionButton`: The user clicked on an action button in the Clips player.\n- `pageActionButton`: The user clicked on the action button in the Story player.\n- `tap`: The user tapped on the next or previous Story Page to navigate to this page.\n- `instanceMethod`: `Storyteller.openStory()` or `Storyteller.openPage()` was called to open a Story or a Page.\n- `loop`: The user completes a loop of a Clip naturally or by scrubbing to the end of the Clip's duration.\n- `categoryListTap` / `categoryBackTap`: Navigation into or back from a category.\n- `searchTap`: Content opened from Search.\n- `embeddedClipsOpen`: Initial Embedded Clips opening.\n- `pullToRefresh` / `reloadMethod`: Content opened after a refresh or reload.\n\n`openedReason` can be included on `openedStory`, `openedClip`, `openedPage`, `storyInstructionsScreenViewed`, `completedLoop`, `openedAd` and `openedSheet` events, and on other events when provided by the\nnative SDK. Android uses `storyInstructionScreenViewed` for the instruction\nscreen event.\n\n### Dismissed Reason\n\nThe `dismissedReason` (`String?`) value states the way the user dismissed a story or clip. The possible values for this are `closeButtonTapped` (the user tapped close to dismiss the story); `swipedDown` (the user swiped down to dismiss the story); `swipedFirstStory` (the user swiped the first story to dismiss it); `swipedFinalStory` (the user swiped the final story to dismiss it); `skippedFinalPage` (the user tapped to skip the final page of the final story); `completedFinalPage` (the user completed the final page of the final story) and `backButtonTapped` (the user tapped the back button to dismiss the clip).\n\nThe reason can also accompany ad dismissal. Native versions can provide\nadditional values, including `backTapped`, `pageActionButton`, and\n`deepLinkReplacement`; retain the string instead of rejecting unknown values.\n\n### Duration Viewed\n\nThe `durationViewed` (`double?`) is the duration the user viewed the story or clip for in milliseconds. This is measured from the most recent `openedStory` or `openedClip` event with an Opened Reason of `storyListTap`, `deepLink`, `card`, `pageActionButton`, `clipActionButton` or `clipListTap`.\n\nThis timer is reset after any `dismissedStory` or `dismissedClip` events.\n\nFor `finishedClip`, this describes the active Clip viewing session in\nmilliseconds. Heartbeat events use accumulated active watch time instead of\nthe Story/player session timer described above.\n\n### Pages Viewed Count\n\nThe `pagesViewedCount` (`int?`) is the total number of pages a user has viewed since the most recent `openedStory` event with an Opened Reason of `storyListTap`, `pageActionButton`, `clipActionButton`, `card` or `deepLink`. This count is reset after any `dismissedStory` events.\n\n### Quartile and External Search Keys\n\nThe `quartile` (`int?`) is the playback progress milestone (25, 50, or 75) on\norganic quartile events. `actionType`, `searchResultId`, `searchResultTitle`,\nand `searchResultSource` (`String?`) identify a configured external Search\nresult. These properties are new in Flutter 12.1.2.\n\n### Content Length\n\nThe `contentLength` (`int?`) is the total duration of the page content in seconds.\n\n### Share Method\n\nThe `shareMethod` (`String?`) is the component name of the app which the user has selected for sharing.\n\n### Advertisers Name\n\nThe `advertiserName` (`String?`) is the name of the advertiser for a particular ad. This is only included for ad events.\n\n### Ad ID\n\nThe `adId` (`String?`) is the ad ID if an event is associated with an ad.\n\n### Ad Type\n\nThe `adType` (`String?`) is the type of component on which the ad is displayed. This can have values `stories` or `clips`.\n\n### Ad Format\n\nThe `adFormat` (`String?`) represents the format of the Ad that was displayed. Possible values can be `customNative`, `native`, `banner`.\n\n### Ad Placement\n\nThe `adPlacement` (`String?`) represents the placement of the ad. It can be either `Between Stories`, `Between Pages` or `Between Clips`. This is only included for ad events.\n\n### Ad Strategy\n\nThe `adStrategy` (`String?`) represents the strategy used to display the ads. It can have the following values: `Between Stories`, `Between Pages`, `Between Stories and Pages`, `Between Clips`.\n\n### Ad Response Identifier\n\nThe `adResponseIdentifier` (`String?`) represents the response identifier attached to the ad that was received from the ad provider. Used for debugging ad targeting.\n\n### Ad Index\n\nThe `adIndex` (`int?`) represents the order of the Ad within the displayed Ads in a Story or Clip collection (1 for the first Ad, 2 for the second, etc.). This field is included in all ad-related events.\n\n### Ad Request ID\n\nThe `adRequestId` (`String?`) identifies a native ad request when the SDK provides one.\n\n### Ad Slot Type\n\nThe `adSlotType` (`String?`) describes the native ad slot, such as an interstitial or bottom banner placement, when available.\n\n### Ad Unit ID\n\nThe `adUnitId` (`String?`) is the Google Ad Manager ad unit used for the request when available.\n\n### Paid Ad Fields\n\nPaid ad and mediation callbacks can include `adSource`, `adSourceName`, `adSourceId`, `adSourceInstanceName`, `adSourceInstanceId`, `adMediationGroupName`, `adMediationAbTestName`, `adMediationAbTestVariant`, `adAdapterLatencyMillis`, `adErrorCode`, `adErrorDomain`, `adErrorMessage`, `adValueMicros`, `adCurrencyCode`, and `adValuePrecision`.\n\n### Ad Request Lifecycle and Revenue\n\n**Event Types:** `adOpportunity`, `adRequested`, `adLoaded`,\n`adFailedToLoad`, `adPaid`\n\nThese native events describe an eligible ad opportunity, request, successful\nload, load failure, or paid impression. They depend on the configured ad\nintegration and provider; an opportunity does not guarantee a request, fill,\nor revenue. Correlate delivered `playerSessionId`, `adRequestId`, `adSlotType`,\n`adUnitId`, source/error fields, and paid value/currency fields when present.\n\n### Ad Session Summary Fields\n\nThe `adSessionSummary` event can include `adSessionOpportunitiesCount`, `adSessionRequestsCount`, `adSessionLoadsCount`, `adSessionFailedToLoadCount`, `adSessionPaidCount`, `adSessionRevenueMicros`, and `adSessionCurrencyCode`.\n\n### Audio Toggle Fields\n\nThe `audioToggleFrom` and `audioToggleTo` (`String?`) fields describe the previous and next audio states when an event represents an audio state change.\n\n### Mute and Analytics Flags\n\nThe `isMuted` (`bool?`) and `excludeFromAnalytics` (`bool?`) fields expose native mute and analytics exclusion state when provided by the SDK.\n\n### Is Initial Buffering\n\nThe `isInitialBuffering` (`bool?`) value is returned if the buffering happens at the start of playback for that page. This is only included for `bufferingStarted` and `bufferingEnded` events.\n\n### Time Since Buffering Began\n\nThe `timeSinceBufferingBegan` (`double?`) value is the duration the current buffering lasted for in milliseconds. This is only included for `bufferingEnded` events.\n\n### Categories\n\nThe `categories` (`List<String>?`) value contains the category names supplied by\nthe native SDK for an event. It can be included on Story, Clip, Card, Ad and\nSearch events when their native payload provides it.\n\nAvailability depends on the event, platform, bundled native SDK version and\ncontent context. Flutter forwards the native list without restricting it to\n`openedStory`. Handle `null` when the field is absent and an empty list when no\ncategory names are supplied; do not assume every event listed in this guide\nincludes it.\n\n### Poll Answer\n\nThe `pollAnswerId` (`String?`) is the ID of the answer the user selected when voting. This is only included on `votedPoll` events.\n\n### Trivia Quiz ID\n\nThe `triviaQuizId` (`String?`) is the ID of the trivia quiz that was completed or answered. This is only included on `triviaQuizQuestionAnswered` and `triviaQuizCompleted` events.\n\n### Trivia Quiz Title\n\nThe `triviaQuizTitle` (`String?`) is the title of the trivia quiz that was completed or answered. This is only included on `triviaQuizQuestionAnswered` and `triviaQuizCompleted` events.\n\n### Trivia Quiz Score\n\nThe `triviaQuizScore` (`int?`) value is the score of the trivia quiz that was completed. This is only included on `triviaQuizCompleted` events.\n\n### Trivia Quiz Question ID\n\nThe `triviaQuizQuestionId` (`String?`) is the ID of the trivia quiz question which was answered. This is only included on `triviaQuizQuestionAnswered` events.\n\n### Trivia Quiz Answer ID\n\nThe `triviaQuizAnswerId` (`String?`) is the ID of the selected trivia quiz answer. This is only included on `triviaQuizQuestionAnswered` events.\n\n### Clip ID\n\nThe `clipId` (`String?`) is the ID of the clip for which the event occurred.\n\n### Clip Title\n\nThe `clipTitle` (`String?`) is the title of the clip for which the event occurred.\n\n### Clip Index\n\nThe `clipIndex` (`int?`) is the index of the clip in the row or grid at the point it was selected or the index of the clip in the player inside the original row or grid. For Ad events, clip index refers to the index of the clip before the Ad.\n\n### Clip Collection Count\n\nThe `collectionClipCount` (`int?`) is the total number of clips in the collection being viewed.\n\n### Clip Feed Type\n\nThe `clipFeedType` (`String?`) indicates the type of clip feed being viewed. Possible values are `default`, `forYou`, and `following`.\n\n### Clips Viewed\n\nThe `clipsViewed` (`int?`) value is the total number of clips a user has viewed since the most recent openedClip event with an Opened Reason of `clipListTap`, `pageActionButton`, `card`, `clipActionButton` or `deepLink`. This count should be reset after any dismissedClip events.\n\n### Loops Viewed\n\nThe `loopsViewed` (`int?`) is for dismissedClip, the total number of loops (plays of an individual clip) a user has viewed since the most recent openedClip event with an Opened Reason of `clipListTap`, `pageActionButton`, `card`, `clipActionButton` or `deepLink`.\n\nThis count should be reset after any dismissedClip events. For finishedClip, the total number of loops.\n\n### Is Live\n\nThe `isLive` (`bool?`) indicates whether a Clip, Story, or Card is currently being broadcast in real-time and is therefore \"Live\".\n\n### Airplay Enabled\n\nThe `airplayEnabled` (`bool?`) indicates whether the device is currently using AirPlay for audio/video output when the event occurred.\n\n### Clip Has Action\n\nThe `clipHasAction` (`bool?`) value is whether the clip associated with the event contains a primary action.\n\n### Clip Action Text\n\nThe `clipActionText` (`String?`) is the text call to action if the clip associated with the event has a primary action link.\n\n### Clip Action URL\n\nThe `clipActionUrl` (`String?`) is the URL linked to from the primary action if a clip associated with the event has a primary action.\n\n### Clip Action Type\n\nThe `clipActionType` (`String?`) is the type of the primary action on a clip associated with the event. If there is no action button then the value is null.\n\n### Collection\n\nThe `collection` (`String?`) is the ID of the collection if a story or clip is being played from a collection.\n\n### Collection Title\n\n`collectionTitle` (`String?`) is a retained legacy Dart property. Do not rely\non it being populated: the current Android mapper does not supply this key.\nUse the nullable `containerTitle` for the native container title when delivered.\nAndroid supplies `collectionId` as an alias of `collection`; iOS does not\nguarantee that alias, so use `collection` for the collection ID.\n\n### Container Title\n\nThe `containerTitle` (`String?`) is the title of the collection if a story or clip is being played from a collection.\n\n### Category Details\n\nThe `categoryDetails` (`List<StorytellerCategoryDetail>?`) is a list of Category Detail objects. The details are the name, ID, type and placement of the category.\n\n### Category Name\n\nThe `categoryName` (`String?`) of the category being navigated to or dismissed.\n\n### Category ID\n\nThe `categoryId` (`String?`) of the category being navigated to or dismissed.\n\n### Current Category\n\nThe `currentCategory` (`StorytellerCategoryDetail?`) is the category associated\nwith the current placement when supplied. It exposes `name`, `id`, `type`,\nand nullable `placement`.\n\nIt can be supplied on Story and ad events; check for null before reading it.\n\n### Captions Enabled\n\nThe `captionsEnabled` (`bool?`) property indicates whether captions are currently enabled for the Clips Player. This can be included in Clip analytics events and represents the state of captions at the time the event occurred.\n\n### Start Position\n\nThe `startPosition` (`int?`) represents the playback position, in milliseconds, when the user started scrubbing the clip. This is only included for `scrubbedClip` events.\n\n### End Position\n\nThe `endPosition` (`int?`) represents the playback position, in milliseconds, when the user stopped scrubbing the clip. This is only included for `scrubbedClip` events.\n\n### Eyebrow\n\nThe `eyebrow` (`String?`) is a text field displayed on stories and clips, typically shown above the main title as a subtitle or descriptor.\n\n### Scrub Direction\n\nThe `scrubDirection` (`String?`) indicates whether the user scrubbed to a position forward or backward on a Clip. Possible values are `forward` and `backward`. This is only included for `scrubbedClip` events.\n\n### Completion Type\n\nThe `completionType` (`String?`) indicates how a user finished watching a Clip. Possible values are `natural` and `scrubbed`.\nThis is included for `finishedClip` and `completedLoop` events.\n\n### Sheet ID\n\nThe `sheetId` (`String?`) - The ID of the Sheet for which the event occurred.\n\n### Sheet Size\n\nThe `sheetSize` (`int?`) - The height of the Sheet for which the event occurred. Possible values are `50`, `75`, and `100` (representing % of screen height).\n\n### Sheet Title\n\nThe `sheetTitle` (`String?`) - The title of the Sheet for which the event occurred.\n\n### Search From\n\nThe `searchFrom` (`String?`) identifies the Search entry point. Native values\ninclude `clipsPlayer`, `storiesPlayer`, and `instanceMethod`.\n\n### Is Suggestion\n\nThe `isSuggestion` (`bool?`) indicates whether the Search for which the event is recorded used a suggested Search.\n\n> Note: When filters are opened (event), the Is Suggestion value should reflect what was used for the initial search performed before opening filters.\n\n### Initial Input\n\nThe `initialInput` (`String?`) this property tracks the input at the moment the suggestion was used.\n\n### Search Filter\n\nThe `searchFilter` (`String?`) property includes the content type and date posted used by search filter.\n\n### Search Sort\n\nThe `searchSort` (`String?`) field indicates method by which the relevant Story / Clip's search results were sorted.\n\n### Search Term\n\nThe `searchTerm` (`String?`) by which the Clips / Stories were searched, either entered in the search bar by the user or selected / filled from search suggestions.\n\n### Action Text\n\nThe `actionText` (`String?`) property is used for general action text and can apply to both page actions and clip actions depending on the placement context.\n\n### Action Class\n\nThe `actionClass` (`String?`) identifies whether the action button that was tapped is a primary or secondary action. Possible values are `primary` and `secondary`. This is only included for `actionButtonTapped` events.\n\n### Action Index\n\nThe `actionIndex` (`int?`) is the 1-based index of the secondary action that was tapped. This is only included for `actionButtonTapped` events when a secondary action is tapped.\n\n### Tapped Clip Action Text\n\nThe `tappedClipActionText` (`String?`) is the text of the specific action button (primary or secondary) that was tapped. This is only included for `actionButtonTapped` events.\n\n### Tapped Clip Action URL\n\nThe `tappedClipActionUrl` (`String?`) is the URL of the specific action button (primary or secondary) that was tapped. This is only included for `actionButtonTapped` events.\n\n### Tapped Clip Action Type\n\nThe `tappedClipActionType` (`String?`) is the type of the specific action button (primary or secondary) that was tapped. This is only included for `actionButtonTapped` events.\n\n### Has Secondary Actions\n\nThe `clipHasSecondaryActions` (`bool?`) indicates whether the clip has any secondary actions. This can be included in Clip analytics events.\n\n### Secondary Actions Text\n\nThe `clipSecondaryActionsText` (`List<String>?`) is an array of all the text CTAs on secondary actions for the clip. This can be included in Clip analytics events when the clip has secondary actions.\n\n### Secondary Action URLs\n\nThe `clipSecondaryActionUrls` (`List<String>?`) is an array of all the URLs on secondary actions for the clip. This can be included in Clip analytics events when the clip has secondary actions.\n\n### Secondary Action Types\n\nThe `clipSecondaryActionTypes` (`List<String>?`) is an array of all the types of secondary actions for the clip. This can be included in Clip analytics events when the clip has secondary actions.\n\n### Card ID\n\nThe `cardId` (`String?`) is the ID of the Card for which the event occurred. This is included for `cardTapped` event.\n\n### Card Action Type\n\nThe `cardActionType` (`String?`) is the type of the action on the Card. This is included for `cardTapped` event.\n\n### Card Action URL\n\nThe `cardActionUrl` (`String?`) is the URL for the link if the Card has an action. This is included for `cardTapped` event.\n\n### Card Aspect Ratio\n\nThe `cardAspectRatio` (`String?`) is the aspect ratio of the Card. This is included for `cardTapped` event.\n\n### Card Background Type\n\nThe `cardBackgroundType` (`String?`) is the type of background on the Card. This can have the value `image` or `video`. This is included for `cardTapped` event.\n\n### Card Collection ID\n\nThe `cardCollectionId` (`String?`) is the ID of the collection the Card belongs to. This is included for `cardTapped` event.\n\n### Card Index\n\nThe `cardIndex` (`int?`) is the index of the Card in the collection.\n\n> Note: This value is 1-based.\n\n### Card Subtitle\n\nThe `cardSubtitle` (`String?`) is the subtitle text displayed on the Card. This is included for `cardTapped` event.\n\n### Card Title\n\nThe `cardTitle` (`String?`) is the title text displayed on the Card. This is included for `cardTapped` event.\n\n### Has Button\n\nThe `hasButton` (`bool?`) indicates whether the Card displayed a button. This is included for `cardViewed` and `cardTapped` events.\n\n### Followable Category Limit Dialogue\n\nThe `followableCategoryLimitDialogue` (`String?`) is the text displayed in the Followable Category Limit dialogue box. This is included for `followableCategoryLimitShown`, `followableCategoryLimitActionButtonTapped`, and `followableCategoryLimitDismissed` events.\n\n### Followable Category Limit Action Text\n\nThe `followableCategoryLimitActionText` (`String?`) is the text displayed on the customisable action button in the Followable Category Limit dialogue box. This is included for `followableCategoryLimitShown`, `followableCategoryLimitActionButtonTapped`, and `followableCategoryLimitDismissed` events.\n\n### Followable Category Limit Action URL\n\nThe `followableCategoryLimitActionUrl` (`String?`) is the URL associated with the customisable action button in the Followable Category Limit dialogue box. This is included for `followableCategoryLimitShown`, `followableCategoryLimitActionButtonTapped`, and `followableCategoryLimitDismissed` events.\n\n### Metadata\n\nThe `metadata` (`Map<String, String>?`) contains custom metadata associated with the content for which the event occurred. For Clip events, this contains the metadata from the associated clip. For Story events, this contains the metadata from the associated page. It may be absent when content has no metadata.\n\n\nHere's a complete example showing how to implement comprehensive analytics tracking:\n\n```dart\nimport 'package:storyteller_sdk/storyteller_sdk.dart';\nimport 'dart:async';\n\nclass StorytellerAnalytics {\n StreamSubscription<StorytellerUserActivityEvent>? _analyticsSubscription;\n\n void initialize() {\n if (_analyticsSubscription != null) return;\n _analyticsSubscription = Storyteller.onUserActivityOccurred.listen(\n (event) {\n // Log all events\n print('Storyteller Event: ${event.type}');\n\n // Handle specific event types\n switch (event.type) {\n case 'openedStory':\n _handleStoryOpened(event.data);\n break;\n case 'openedClip':\n _handleClipOpened(event.data);\n break;\n case 'completedPage':\n _handlePageCompleted(event.data);\n break;\n case 'likedClip':\n _handleClipLiked(event.data);\n break;\n // Add more cases as needed\n }\n\n // Forward to your analytics platform\n _forwardToAnalytics(event);\n },\n onError: (error) {\n print('Analytics error: $error');\n },\n );\n }\n\n void _handleStoryOpened(StorytellerUserActivityData data) {\n print('Story Opened: ${data.storyTitle}');\n print('Read Status: ${data.storyReadStatus}');\n print('Categories: ${data.categories}');\n }\n\n void _handleClipOpened(StorytellerUserActivityData data) {\n print('Clip Opened: ${data.clipTitle}');\n print('Collection: ${data.collection}');\n print('Is Live: ${data.isLive}');\n }\n\n void _handlePageCompleted(StorytellerUserActivityData data) {\n print('Page Completed: ${data.pageTitle}');\n print('Duration: ${data.contentLength}s');\n }\n\n void _handleClipLiked(StorytellerUserActivityData data) {\n print('Clip Liked: ${data.clipTitle}');\n }\n\n void _forwardToAnalytics(StorytellerUserActivityEvent event) {\n // Forward to Firebase, Segment, Mixpanel, etc.\n // analytics.track(event.type, {\n // 'storyId': event.data.storyId,\n // 'clipId': event.data.clipId,\n // ...event.data.fields,\n // // ... other relevant fields\n // });\n }\n\n Future<void> dispose() async {\n await _analyticsSubscription?.cancel();\n _analyticsSubscription = null;\n }\n}\n```\n\n**Usage:**\n\n```dart\nimport 'package:flutter/widgets.dart';\n\nFuture<void> main() async {\n WidgetsFlutterBinding.ensureInitialized();\n\n // Subscribe before initialization/content interactions.\n final analytics = StorytellerAnalytics();\n analytics.initialize();\n\n final result = await Storyteller.initialize('your-api-key');\n if (!result.success) {\n await analytics.dispose();\n throw StateError(result.message ?? 'Storyteller initialization failed');\n }\n\n runApp(MyApp());\n}\n```\n\n## Verifying Your Integration\n\nOpen a known published Story/Clip and log the event type and raw payload. A\nsuccessful interaction produces `openedStory` or `openedClip`; closing the\nplayer produces its dismissal event. Retain one listener per owning service\nand cancel it during disposal to avoid duplicate forwarding.\n\nIf no events arrive, check that initialization succeeded, the listener was\nattached before content interactions, and `enableUserActivityTracking` is\ntrue. If IDs or titles are null, inspect `await Storyteller.eventTrackingOptions()`.\nPassing `StorytellerEventTrackingOptions()` sets `enableFullVideoAnalytics` to\nfalse by default and native code filters content identifiers/titles. Omitting\nthe options argument to `initialize` currently defaults this flag to true on\nboth platform bridges. Set it explicitly according to your tracking policy;\nsee [Privacy and Tracking](PrivacyAndTracking.md).\n\nCustomer callbacks, Storyteller ingestion, and ad-provider tracking are\nseparate destinations. Receiving an event does not prove that it was ingested\nor that an ad filled. For more diagnostic steps, see\n[Troubleshooting](Troubleshooting.md).\n", "copy_markdown_include_header": false, "base_path": "", "ai_dir": "ai", "missing_payload_behavior": "empty"}