-
Notifications
You must be signed in to change notification settings - Fork 93
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add screen engagement tracking of time spent and list items scrolled …
- Loading branch information
1 parent
e06a0a8
commit a4a1c9c
Showing
30 changed files
with
806 additions
and
7 deletions.
There are no files selected for viewing
Submodule Examples
updated
8 files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. | ||
// | ||
// This program is licensed to you under the Apache License Version 2.0, | ||
// and you may not use this file except in compliance with the Apache License | ||
// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at | ||
// http://www.apache.org/licenses/LICENSE-2.0. | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the Apache License Version 2.0 is distributed on | ||
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
// express or implied. See the Apache License Version 2.0 for the specific | ||
// language governing permissions and limitations there under. | ||
|
||
import Foundation | ||
|
||
class ScreenEnd: SelfDescribingAbstract { | ||
|
||
override var schema: String { | ||
return kSPScreenEndSchema | ||
} | ||
|
||
override var payload: [String : Any] { | ||
return [:] | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
Sources/Core/ScreenViewTracking/ListItemViewModifier.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. | ||
// | ||
// This program is licensed to you under the Apache License Version 2.0, | ||
// and you may not use this file except in compliance with the Apache License | ||
// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at | ||
// http://www.apache.org/licenses/LICENSE-2.0. | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the Apache License Version 2.0 is distributed on | ||
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
// express or implied. See the Apache License Version 2.0 for the specific | ||
// language governing permissions and limitations there under. | ||
|
||
#if canImport(SwiftUI) | ||
|
||
import SwiftUI | ||
import Foundation | ||
|
||
@available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, *) | ||
@available(watchOS, unavailable) | ||
internal struct ListItemViewModifier: ViewModifier { | ||
let index: Int | ||
let itemsCount: Int? | ||
let trackerNamespace: String? | ||
|
||
/// Get tracker by namespace if configured, otherwise return the default tracker | ||
private var tracker: TrackerController? { | ||
if let namespace = trackerNamespace { | ||
return Snowplow.tracker(namespace: namespace) | ||
} else { | ||
return Snowplow.defaultTracker() | ||
} | ||
} | ||
|
||
/// Modifies the view to track the list item view when it appears | ||
func body(content: Content) -> some View { | ||
content.onAppear { | ||
trackListItemView() | ||
} | ||
} | ||
|
||
func trackListItemView() { | ||
let event = ListItemView(index: index) | ||
event.itemsCount = itemsCount | ||
|
||
if let tracker = tracker { | ||
_ = tracker.track(event) | ||
} else { | ||
logError(message: "List item view not tracked – tracker not initialized.") | ||
} | ||
} | ||
} | ||
|
||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. | ||
// | ||
// This program is licensed to you under the Apache License Version 2.0, | ||
// and you may not use this file except in compliance with the Apache License | ||
// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at | ||
// http://www.apache.org/licenses/LICENSE-2.0. | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the Apache License Version 2.0 is distributed on | ||
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
// express or implied. See the Apache License Version 2.0 for the specific | ||
// language governing permissions and limitations there under. | ||
|
||
import Foundation | ||
|
||
class ScreenSummaryState: State { | ||
|
||
static var dateGenerator: () -> TimeInterval = { Date().timeIntervalSince1970 } | ||
|
||
private var lastUpdateTimestamp: TimeInterval = ScreenSummaryState.dateGenerator() | ||
var foregroundSeconds: TimeInterval = 0 | ||
var backgroundSeconds: TimeInterval = 0 | ||
var lastItemIndex: Int? | ||
var itemsCount: Int? | ||
var minYOffset: Int? | ||
var maxYOffset: Int? | ||
var minXOffset: Int? | ||
var maxXOffset: Int? | ||
var contentHeight: Int? | ||
var contentWidth: Int? | ||
|
||
var data: [String: Any] { | ||
var data: [String: Any] = [ | ||
"foreground_sec": round(foregroundSeconds * 100) / 100, | ||
"background_sec": round(backgroundSeconds * 100) / 100 | ||
] | ||
if let lastItemIndex = lastItemIndex { data["last_item_index"] = lastItemIndex } | ||
if let itemsCount = itemsCount { data["items_count"] = itemsCount } | ||
if let minXOffset = minXOffset { data["min_x_offset"] = minXOffset } | ||
if let maxXOffset = maxXOffset { data["max_x_offset"] = maxXOffset } | ||
if let minYOffset = minYOffset { data["min_y_offset"] = minYOffset } | ||
if let maxYOffset = maxYOffset { data["max_y_offset"] = maxYOffset } | ||
if let contentHeight = contentHeight { data["content_height"] = contentHeight } | ||
if let contentWidth = contentWidth { data["content_width"] = contentWidth } | ||
return data | ||
} | ||
|
||
func updateTransitionToForeground() { | ||
let currentTimestamp = ScreenSummaryState.dateGenerator() | ||
|
||
backgroundSeconds += currentTimestamp - lastUpdateTimestamp | ||
lastUpdateTimestamp = currentTimestamp | ||
} | ||
|
||
func updateTransitionToBackground() { | ||
let currentTimestamp = ScreenSummaryState.dateGenerator() | ||
|
||
foregroundSeconds += currentTimestamp - lastUpdateTimestamp | ||
lastUpdateTimestamp = currentTimestamp | ||
} | ||
|
||
func updateForScreenEnd() { | ||
let currentTimestamp = ScreenSummaryState.dateGenerator() | ||
|
||
foregroundSeconds += currentTimestamp - lastUpdateTimestamp | ||
lastUpdateTimestamp = currentTimestamp | ||
} | ||
|
||
func updateWithListItemView(_ event: ListItemView) { | ||
lastItemIndex = max(event.index, lastItemIndex ?? 0) | ||
if let totalItems = event.itemsCount { | ||
self.itemsCount = max(totalItems, self.itemsCount ?? 0) | ||
} | ||
} | ||
|
||
func updateWithScrollChanged(_ event: ScrollChanged) { | ||
if let yOffset = event.yOffset { | ||
var maxYOffset = yOffset | ||
if let viewHeight = event.viewHeight { maxYOffset += viewHeight } | ||
|
||
minYOffset = min(yOffset, minYOffset ?? yOffset) | ||
self.maxYOffset = max(maxYOffset, self.maxYOffset ?? maxYOffset) | ||
} | ||
if let xOffset = event.xOffset { | ||
var maxXOffset = xOffset | ||
if let viewWidth = event.viewWidth { maxXOffset += viewWidth } | ||
|
||
minXOffset = min(xOffset, minXOffset ?? xOffset) | ||
self.maxXOffset = max(maxXOffset, self.maxXOffset ?? maxXOffset) | ||
} | ||
if let height = event.contentHeight { contentHeight = max(height, contentHeight ?? 0) } | ||
if let width = event.contentWidth { contentWidth = max(width, contentWidth ?? 0) } | ||
} | ||
|
||
} |
93 changes: 93 additions & 0 deletions
93
Sources/Core/ScreenViewTracking/ScreenSummaryStateMachine.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
// Copyright (c) 2013-2023 Snowplow Analytics Ltd. All rights reserved. | ||
// | ||
// This program is licensed to you under the Apache License Version 2.0, | ||
// and you may not use this file except in compliance with the Apache License | ||
// Version 2.0. You may obtain a copy of the Apache License Version 2.0 at | ||
// http://www.apache.org/licenses/LICENSE-2.0. | ||
// | ||
// Unless required by applicable law or agreed to in writing, | ||
// software distributed under the Apache License Version 2.0 is distributed on | ||
// an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
// express or implied. See the Apache License Version 2.0 for the specific | ||
// language governing permissions and limitations there under. | ||
|
||
import Foundation | ||
|
||
class ScreenSummaryStateMachine: StateMachineProtocol { | ||
static var identifier: String { return "ScreenSummaryContext" } | ||
var identifier: String { return ScreenSummaryStateMachine.identifier } | ||
|
||
var subscribedEventSchemasForEventsBefore: [String] { | ||
return [kSPScreenViewSchema] | ||
} | ||
|
||
var subscribedEventSchemasForTransitions: [String] { | ||
return [kSPScreenViewSchema, kSPScreenEndSchema, kSPForegroundSchema, kSPBackgroundSchema, kSPListItemViewSchema, kSPScrollChangedSchema] | ||
} | ||
|
||
var subscribedEventSchemasForEntitiesGeneration: [String] { | ||
return [kSPScreenEndSchema, kSPForegroundSchema, kSPBackgroundSchema] | ||
} | ||
|
||
var subscribedEventSchemasForPayloadUpdating: [String] { | ||
return [] | ||
} | ||
|
||
var subscribedEventSchemasForAfterTrackCallback: [String] { | ||
return [] | ||
} | ||
|
||
var subscribedEventSchemasForFiltering: [String] { | ||
return [kSPListItemViewSchema, kSPScrollChangedSchema, kSPScreenEndSchema] | ||
} | ||
|
||
func eventsBefore(event: Event) -> [Event]? { | ||
return [ScreenEnd()] | ||
} | ||
|
||
func transition(from event: Event, state currentState: State?) -> State? { | ||
if event is ScreenView { | ||
return ScreenSummaryState() | ||
} | ||
else if let state = currentState as? ScreenSummaryState { | ||
switch event { | ||
case is Foreground: | ||
state.updateTransitionToForeground() | ||
case is Background: | ||
state.updateTransitionToBackground() | ||
case is ScreenEnd: | ||
state.updateForScreenEnd() | ||
case let itemView as ListItemView: | ||
state.updateWithListItemView(itemView) | ||
case let scrollChanged as ScrollChanged: | ||
state.updateWithScrollChanged(scrollChanged) | ||
default: | ||
break | ||
} | ||
} | ||
return currentState | ||
} | ||
|
||
func entities(from event: InspectableEvent, state: State?) -> [SelfDescribingJson]? { | ||
guard let state = state as? ScreenSummaryState else { return nil } | ||
|
||
return [ | ||
SelfDescribingJson(schema: kSPScreenSummarySchema, andData: state.data) | ||
] | ||
} | ||
|
||
func payloadValues(from event: InspectableEvent, state: State?) -> [String : Any]? { | ||
return nil | ||
} | ||
|
||
func filter(event: InspectableEvent, state: State?) -> Bool? { | ||
if event.schema == kSPScreenEndSchema { | ||
return state != nil | ||
} | ||
// do not track list item view or scroll changed events | ||
return false | ||
} | ||
|
||
func afterTrack(event: InspectableEvent) { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.