Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions Sources/Video.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,28 @@ struct TrackedVideo {
}

class VideoManager: Sampler {

var trackedVideos: Dictionary<String, TrackedVideo> = [:]

override func sampleFn(key: String) -> Bool {
if trackedVideos[key] == nil {
return false
}
return (trackedVideos[key]?.isPlaying)!
}

override func heartbeatFn(data: Accumulator, enableHeartbeats: Bool) -> Void {
if enableHeartbeats != true {
return
}
let roundedSecs: Int = Int(data.accumulatedTime)
let totalMs: Int = Int(data.totalTime.milliseconds())

guard var curVideo = trackedVideos[data.key] else { return }
guard var curVideo = trackedVideos[data.key] else {
os_log("Skipping heartbeat for video %s because it is not in the tracked videos list", log: OSLog.tracker, type:.debug, data.key)
return
}

let event = Heartbeat(
"vheartbeat",
url: curVideo.url,
Expand All @@ -45,7 +49,7 @@ class VideoManager: Sampler {
curVideo._heartbeatsSent += 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mutation is why we need curVideo to be a var.

trackedVideos[curVideo.key] = curVideo
}

func trackPlay(url: String, urlref: String, vId: String, duration: TimeInterval, metadata: ParselyMetadata?, extra_data: Dictionary<String, Any>?, idsite: String) -> Void {
trackPause()
let eventArgs = generateEventArgs(url: url, urlref: urlref, metadata: metadata, extra_data: extra_data, idsite: idsite)
Expand Down Expand Up @@ -74,7 +78,7 @@ class VideoManager: Sampler {
trackedVideos[curVideo!.key] = curVideo
}
}

func reset(url: String, vId: String) {
os_log("Reset video accumulator for url %s and vId %s", log: OSLog.tracker, type:.debug, url, vId)
trackPause()
Expand Down Expand Up @@ -104,13 +108,13 @@ class VideoManager: Sampler {
hasStartedPlaying: false,
eventArgs: _eventArgs,
_heartbeatsSent: 0)

trackKey(key: key, contentDuration: duration, eventArgs:_eventArgs)
}

return trackedVideos[key]!
}

private func createVideoTrackingKey(vId: String, url: String) -> String {
return url + "::" + vId
}
Expand Down
259 changes: 185 additions & 74 deletions Tests/VideoTests.swift
Original file line number Diff line number Diff line change
@@ -1,100 +1,211 @@
import XCTest
import Nimble
@testable import ParselyAnalytics
import XCTest

class VideoTests: ParselyTestCase {

let testVideoId: String = "videoId"
let testUrl: String = "testurl"
var videoManager: VideoManager?

override func setUp() {
super.setUp()
videoManager = VideoManager(trackerInstance: parselyTestTracker)
var testVideoKey: String {
return "\(testUrl)::\(testVideoId)"
}

func testTrackPlay() {
XCTAssertEqual(videoManager!.trackedVideos.count, 0,
"videoManager.accumulators should be empty before calling trackPlay")
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: nil, extra_data: nil, idsite: Parsely.testAPIKey)
XCTAssertEqual(videoManager!.trackedVideos.count, 1,
"A call to trackPlay should populate videoManager.accumulators with one object")
let videoManager = VideoManager(trackerInstance: parselyTestTracker)

expect(videoManager.trackedVideos).to(beEmpty())

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: nil,
extra_data: nil,
idsite: Parsely.testAPIKey
)

expect(videoManager.trackedVideos).to(haveCount(1))
}

func testTrackPause() {
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: nil, extra_data: nil, idsite: Parsely.testAPIKey)
videoManager!.trackPause()
XCTAssertEqual(videoManager!.trackedVideos.count, 1,
"A call to trackPause should not remove an accumulator from videoManager.accumulators")
let videoManager = VideoManager(trackerInstance: parselyTestTracker)

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: nil,
extra_data: nil,
idsite: Parsely.testAPIKey
)

videoManager.trackPause()

expect(videoManager.trackedVideos).to(haveCount(1))
}

func testReset() {
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: nil, extra_data: nil, idsite: Parsely.testAPIKey)
videoManager!.reset(url: testUrl, vId: testVideoId)
XCTAssertNotNil(videoManager!.samplerTimer,
"videoReset should run successfully without the VideoManager instance being paused")
XCTAssertEqual(videoManager!.trackedVideos.count, 0,
"A call to Parsely.track.videoManager.reset should remove a video from videoManager.trackedVideos")
let videoManager = VideoManager(trackerInstance: parselyTestTracker)

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: nil,
extra_data: nil,
idsite: Parsely.testAPIKey
)

videoManager.reset(url: testUrl, vId: testVideoId)

expect(videoManager.samplerTimer).toNot(beNil())
expect(videoManager.trackedVideos).to(beEmpty())
}
func testUpdateVideoEventArgs() {
let testSectionFirst: String = "sectionname"
let testSectionSecond: String = "adifferentsection"
let firstTestMetadata: ParselyMetadata = ParselyMetadata(canonical_url: testUrl, pub_date: Date(), title: "test",
authors: nil, image_url: nil, section: testSectionFirst,
tags: nil, duration: nil)
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: firstTestMetadata, extra_data: nil, idsite: Parsely.testAPIKey)
let testTrackedVideo: TrackedVideo = videoManager!.trackedVideos.values.first!
let actualMetadata: ParselyMetadata = testTrackedVideo.eventArgs["metadata"]! as! ParselyMetadata
XCTAssertEqual(actualMetadata.section, testSectionFirst,
"The section metadata stored for a video after a call to parsely.track.videoManager.trackPlay " +
"should match the section metadata passed to that call.")
let secondTestMetadata: ParselyMetadata = ParselyMetadata(canonical_url: testUrl, pub_date: Date(), title: "test",
authors: nil, image_url: nil, section: testSectionSecond,
tags: nil, duration: nil)
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: secondTestMetadata, extra_data: nil, idsite: Parsely.testAPIKey)
let secondTestTrackedVideo: TrackedVideo = videoManager!.trackedVideos.values.first!
let secondActualMetadata: ParselyMetadata = secondTestTrackedVideo.eventArgs["metadata"]! as! ParselyMetadata
XCTAssertEqual(secondActualMetadata.section, testSectionSecond,
"The section metadata stored for a preexisting video after a call to parsely.track.videoManager.trackPlay " +
"should match the section metadata passed to that call.")

func testUpdateVideoEventArgs() throws {
let videoManager = VideoManager(trackerInstance: parselyTestTracker)

let testSectionFirst = "sectionname"
let testSectionSecond = "adifferentsection"
let firstTestMetadata = ParselyMetadata(
canonical_url: testUrl,
pub_date: Date(),
title: "test",
authors: nil,
image_url: nil,
section: testSectionFirst,
tags: nil,
duration: nil
)

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: firstTestMetadata,
extra_data: nil,
idsite: Parsely.testAPIKey
)

let testTrackedVideo = try XCTUnwrap(videoManager.trackedVideos.values.first)
let actualMetadata = try XCTUnwrap(testTrackedVideo.eventArgs["metadata"] as? ParselyMetadata)

expect(actualMetadata.section).to(equal(testSectionFirst))

let secondTestMetadata = ParselyMetadata(
canonical_url: testUrl,
pub_date: Date(),
title: "test",
authors: nil,
image_url: nil,
section: testSectionSecond,
tags: nil,
duration: nil
)

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: secondTestMetadata,
extra_data: nil,
idsite: Parsely.testAPIKey
)

let secondTestTrackedVideo = try XCTUnwrap(videoManager.trackedVideos.values.first)
let secondActualMetadata = try XCTUnwrap(secondTestTrackedVideo.eventArgs["metadata"] as? ParselyMetadata)

expect(secondActualMetadata.section).to(equal(testSectionSecond))
}

func testSampleFn() {
let testVideoKey: String = testUrl + "::" + testVideoId
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: nil, extra_data: nil, idsite: Parsely.testAPIKey)
let sampleResult: Bool = videoManager!.sampleFn(key: testVideoKey)
XCTAssert(sampleResult,
"After a call to VideoManager.trackPlay, VideoManager.sample should return true for the viewing key")
let videoManager = VideoManager(trackerInstance: parselyTestTracker)

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: nil,
extra_data: nil,
idsite: Parsely.testAPIKey
)

expect(videoManager.sampleFn(key: self.testVideoKey)).to(beTrue())
}

func testSampleFnPaused() {
let testVideoKey: String = testUrl + "::" + testVideoId
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: nil, extra_data: nil, idsite: Parsely.testAPIKey)
videoManager!.trackPause()
let sampleResult: Bool = videoManager!.sampleFn(key: testVideoKey)
XCTAssertFalse(sampleResult,
"After a call to VideoManager.trackPlay followed by a call to VideoManager.trackPause, " +
"VideoManager.sample should return false for the viewing key")
let videoManager = VideoManager(trackerInstance: parselyTestTracker)

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: nil,
extra_data: nil,
idsite: Parsely.testAPIKey
)
videoManager.trackPause()

expect(videoManager.sampleFn(key: self.testVideoKey)).to(beFalse())
}

func testHeartbeatFn() {
let testVideoKey: String = testUrl + "::" + testVideoId
let dummyEventArgs: Dictionary<String, Any> = videoManager!.generateEventArgs(
url: testUrl, urlref: "", extra_data: nil, idsite: Parsely.testAPIKey)
let dummyAccumulator: Accumulator = Accumulator(key: testVideoKey, accumulatedTime: 0, totalTime: 0,
firstSampleTime: Date(),
lastSampleTime: Date(), lastPositiveSampleTime: Date(),
heartbeatTimeout: 0, contentDuration: 0, isEngaged: false,
eventArgs: dummyEventArgs)
videoManager!.trackPlay(url: testUrl, urlref: testUrl, vId: testVideoId, duration: TimeInterval(10),
metadata: nil, extra_data: nil, idsite: Parsely.testAPIKey)
videoManager!.heartbeatFn(data: dummyAccumulator, enableHeartbeats: true)
XCTAssertEqual(parselyTestTracker.eventQueue.length(), 2,
"A call to VideoManager should add two events to eventQueue")
let videoManager = VideoManager(trackerInstance: parselyTestTracker)
let dummyAccumulator = makeAccumulator(videoManager: videoManager, key: testVideoKey, url: testUrl)

videoManager.trackPlay(
url: testUrl,
urlref: testUrl,
vId: testVideoId,
duration: TimeInterval(10),
metadata: nil,
extra_data: nil,
idsite: Parsely.testAPIKey
)
videoManager.heartbeatFn(data: dummyAccumulator, enableHeartbeats: true)

expect(self.parselyTestTracker.eventQueue.list).to(haveCount(2))
}

func testHeartbeatDoesNotCrashIfNoVideoHasBeenTracked() {
let videoManager = VideoManager(trackerInstance: parselyTestTracker)
let dummyAccumulator = makeAccumulator(videoManager: videoManager, key: testVideoKey, url: testUrl)

// Send heart beat without tracking a play first...
videoManager.heartbeatFn(data: dummyAccumulator, enableHeartbeats: true)

// ...and verify that no crash has occurred by performing a simple assertion on the tracker state
expect(self.parselyTestTracker.eventQueue.list).to(beEmpty())
Comment on lines +184 to +185
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't write it in the comment (can add it if you think it would be useful) but of course if there's a crash this code won't run. The assertion is there just so that we can have something in the test.

}
}

extension VideoTests {

func makeAccumulator(videoManager: VideoManager, key: String, url: String) -> Accumulator {
Accumulator(
key: key,
accumulatedTime: 0,
totalTime: 0,
firstSampleTime: Date(),
lastSampleTime: Date(),
lastPositiveSampleTime: Date(),
heartbeatTimeout: 0,
contentDuration: 0,
isEngaged: false,
eventArgs: videoManager.generateEventArgs(
url: url,
urlref: "",
extra_data: nil,
// Intresting: If this is defined as a default argument in the method signature, it won't compile.
idsite: Parsely.testAPIKey
) as [String: Any]
)
}
}