From 6b3d7bf4f04aafb05181a01d1044fab33343b2a2 Mon Sep 17 00:00:00 2001 From: Jamie Sinn Date: Wed, 2 Oct 2024 10:24:36 -0400 Subject: [PATCH] Public Release --- README.md | 1 + bsconfig.json | 3 + components/DevCycle/DevCycleClient.brs | 258 + components/DevCycle/DevCycleOptions.brs | 50 + components/DevCycle/DevCycleSGClient.brs | 135 + components/DevCycle/DevCycleTask.brs | 49 + components/DevCycle/DevCycleTask.xml | 23 + components/DevCycle/DevCycleUser.brs | 101 + components/MainScene.brs | 211 + components/MainScene.xml | 124 + images/channel-poster_fhd.png | Bin 0 -> 15427 bytes images/channel-poster_hd.png | Bin 0 -> 8256 bytes images/channel-poster_sd.png | Bin 0 -> 6401 bytes images/splash-screen_fhd.jpg | Bin 0 -> 57034 bytes images/splash-screen_hd.jpg | Bin 0 -> 33546 bytes images/splash-screen_sd.jpg | Bin 0 -> 18990 bytes makefile | 33 + manifest | 27 + package.json | 30 + setup_tests.sh | 21 + source/Main.brs | 27 + test-app/components/DevCycle/DevCycle.brs | 544 +++ test-app/components/DevCycle/DevCycleTask.brs | 49 + test-app/components/DevCycle/DevCycleTask.xml | 20 + test-app/components/MainScene.brs | 211 + test-app/components/MainScene.xml | 124 + test-app/images/channel-poster_fhd.png | Bin 0 -> 15427 bytes test-app/images/channel-poster_hd.png | Bin 0 -> 8256 bytes test-app/images/channel-poster_sd.png | Bin 0 -> 6401 bytes test-app/images/splash-screen_fhd.jpg | Bin 0 -> 57034 bytes test-app/images/splash-screen_hd.jpg | Bin 0 -> 33546 bytes test-app/images/splash-screen_sd.jpg | Bin 0 -> 18990 bytes test-app/manifest | 27 + test-app/source/Main.brs | 26 + tests/README.md | 56 + tests/bin/RokuWebDriver_mac | Bin 0 -> 7746256 bytes tests/jsLibrary/library/client.js | 152 + tests/jsLibrary/library/rokuLibrary.js | 279 ++ tests/jsLibrary/tests/test_basic.js | 324 ++ tests/jsLibrary/uTest/responses.js | 187 + tests/jsLibrary/uTest/test_client.js | 151 + tests/jsLibrary/uTest/test_rokuLibrary.js | 357 ++ tests/sample/script/main.py | 67 + tests/sample/script/multipleDeviceSample.py | 25 + tests/sample/script/webDriver.py | 98 + yarn.lock | 4257 +++++++++++++++++ 46 files changed, 8047 insertions(+) create mode 100644 README.md create mode 100644 bsconfig.json create mode 100644 components/DevCycle/DevCycleClient.brs create mode 100644 components/DevCycle/DevCycleOptions.brs create mode 100644 components/DevCycle/DevCycleSGClient.brs create mode 100644 components/DevCycle/DevCycleTask.brs create mode 100644 components/DevCycle/DevCycleTask.xml create mode 100644 components/DevCycle/DevCycleUser.brs create mode 100644 components/MainScene.brs create mode 100644 components/MainScene.xml create mode 100644 images/channel-poster_fhd.png create mode 100644 images/channel-poster_hd.png create mode 100644 images/channel-poster_sd.png create mode 100644 images/splash-screen_fhd.jpg create mode 100644 images/splash-screen_hd.jpg create mode 100644 images/splash-screen_sd.jpg create mode 100644 makefile create mode 100644 manifest create mode 100644 package.json create mode 100755 setup_tests.sh create mode 100644 source/Main.brs create mode 100644 test-app/components/DevCycle/DevCycle.brs create mode 100644 test-app/components/DevCycle/DevCycleTask.brs create mode 100644 test-app/components/DevCycle/DevCycleTask.xml create mode 100644 test-app/components/MainScene.brs create mode 100644 test-app/components/MainScene.xml create mode 100644 test-app/images/channel-poster_fhd.png create mode 100644 test-app/images/channel-poster_hd.png create mode 100644 test-app/images/channel-poster_sd.png create mode 100644 test-app/images/splash-screen_fhd.jpg create mode 100644 test-app/images/splash-screen_hd.jpg create mode 100644 test-app/images/splash-screen_sd.jpg create mode 100644 test-app/manifest create mode 100644 test-app/source/Main.brs create mode 100755 tests/README.md create mode 100755 tests/bin/RokuWebDriver_mac create mode 100755 tests/jsLibrary/library/client.js create mode 100755 tests/jsLibrary/library/rokuLibrary.js create mode 100755 tests/jsLibrary/tests/test_basic.js create mode 100755 tests/jsLibrary/uTest/responses.js create mode 100755 tests/jsLibrary/uTest/test_client.js create mode 100755 tests/jsLibrary/uTest/test_rokuLibrary.js create mode 100755 tests/sample/script/main.py create mode 100755 tests/sample/script/multipleDeviceSample.py create mode 100755 tests/sample/script/webDriver.py create mode 100644 yarn.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..1489c40 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# roku-client-sdk diff --git a/bsconfig.json b/bsconfig.json new file mode 100644 index 0000000..10d27a1 --- /dev/null +++ b/bsconfig.json @@ -0,0 +1,3 @@ +{ + "plugins": ["@rokucommunity/bslint"] +} diff --git a/components/DevCycle/DevCycleClient.brs b/components/DevCycle/DevCycleClient.brs new file mode 100644 index 0000000..0412117 --- /dev/null +++ b/components/DevCycle/DevCycleClient.brs @@ -0,0 +1,258 @@ +function DevCycleClient(taskNode as Object) as Object + DevCycleClientObject = { + initialize: sub() + if m.private.initialized = true + return + end if + m.private.config = m.private.getConfig() + m.private.initialized = true + m.private.scheduleNextFlush() + end sub, + identifyUser: sub(user as Object) + m.private.user = DevCycleUser(user) + m.private.getConfig() + end sub, + track: sub(event as Object) + eventType = event.type + + if eventType = invalid OR eventType = "" + return + end if + + if eventType <> "variableEvaluated" AND eventType <> "variableDefaulted" + if m.private.options.disableCustomEventLogging = true + return + else + formattedEvent = { + type: "customEvent", + target: event.target, + value: event.value, + user_id: m.private.user.user_id + } + if event.metaData <> invalid + formattedEvent["metaData"] = event.metaData + end if + formattedEvent["customType"] = eventType + formattedEvent["clientDate"] = CreateObject("roDateTime").ToISOString() + if m.private.config.featureVariationMap <> invalid + formattedEvent["featureVars"] = m.private.config.featureVariationMap + end if + m.private.addCustomEventToQueue(formattedEvent) + end if + else + if m.private.options.disableAutomaticEventLogging = true + return + else + variableEvent = m.private.getVariableEventsFromQueue(eventType, event.target) + + if variableEvent <> invalid + variableEvent.value += 1.0 + variableEvent.clientDate = CreateObject("roDateTime").ToISOString() + variableEvent.metaData = event.metaData + variableEvent.featureVars = m.private.config.featureVariationMap + m.private.addVariableEventToQueue(eventType, variableEvent) + else + formattedEvent = { + type: eventType, + target: event.target, + value: 1.0, + user_id: m.private.user.user_id + } + formattedEvent["clientDate"] = CreateObject("roDateTime").ToISOString() + formattedEvent["featureVars"] = m.private.config.featureVariationMap + formattedEvent["metaData"] = event.metaData + m.private.addVariableEventToQueue(eventType, formattedEvent) + end if + end if + end if + end sub, + resetUser: function() as Object + m.flush() + m.private.user = DevCycleUser({}) + m.private.config = invalid + newConfig = m.private.getConfig() + return newConfig + end function, + flush: sub() + eventsToSend = m.private.eventQueue + m.private.sendEvents(eventsToSend) + m.private.scheduleNextFlush() + end sub, + private: { + sdkKey: taskNode.sdkKey, + user: DevCycleUser(taskNode.user), + options: DevCycleOptions(taskNode.options), + initialized: false, + config: invalid, + taskNode: taskNode, + eventQueue: { + customEvent: [], + variableEvaluated: {}, + variableDefaulted: {} + }, + flushInterval: 10000, + flushTimer: CreateObject("roTimespan"), + scheduleNextFlush: sub() + m.eventQueue["customEvent"] = [] + m.eventQueue["variableEvaluated"] = {} + m.eventQueue["variableDefaulted"] = {} + m.flushTimer.Mark() ' Reset the timer + m.taskNode.flush = true + end sub, + addEventToQueue: sub(eventType as String, events as Object) + if m.eventQueue[eventType] = invalid + m.eventQueue[eventType] = [] + end if + m.eventQueue[eventType] = events + end sub, + getEventsFromQueue: function(eventType as String) as Object + if eventType = "customEvent" and m.eventQueue[eventType] = invalid + return [] + end if + if m.eventQueue[eventType] = invalid + return {} + end if + return m.eventQueue[eventType] + end function, + addCustomEventToQueue: sub(event as Object) + customEvents = m.getEventsFromQueue("customEvent") + customEvents.Push(event) + m.addEventToQueue("customEvent", customEvents) + end sub, + addVariableEventToQueue: sub(eventType as String, event as Object) + if m.eventQueue[eventType][event.target] = invalid + m.eventQueue[eventType][event.target] = {} + end if + m.eventQueue[eventType][event.target] = event + m.addEventToQueue(eventType, m.eventQueue[eventType]) + end sub, + getVariableEventsFromQueue: function(eventType as String, target as String) as Object + if m.eventQueue[eventType] = invalid OR m.eventQueue[eventType][target] = invalid + return invalid + end if + return m.eventQueue[eventType][target] + end function, + sendEvents: sub(events as Object) + if m.options.eventsApiProxyURL <> invalid + url = m.options.eventsApiProxyURL + else + url = "https://events.devcycle.com/v1/events" + end if + + combinedEvents = [] + for each event in events.customEvent + combinedEvents.Push(event) + end for + for each event in events.variableEvaluated + combinedEvents.Push(events.variableEvaluated[event]) + end for + for each event in events.variableDefaulted + combinedEvents.Push(events.variableDefaulted[event]) + end for + + formattedEvents = [] + for each event in combinedEvents + formattedEvent = { + type: event.type, + user_id: event.user_id, + target: event.target, + value: event.value + } + formattedEvent["customType"] = event.customType + formattedEvent["clientDate"] = event.clientDate + formattedEvent["metaData"] = event.metaData + formattedEvent["featureVars"] = event.featureVars + formattedEvents.Push(formattedEvent) + end for + + if formattedEvents.Count() = 0 + return + end if + + numberOfRequests = formattedEvents.Count() / 100 + + for i = 0 to numberOfRequests + requestBody = { + user: m.user, + events: formattedEvents.Slice(i * 100, 100) + } + + urlTransfer = CreateObject("roUrlTransfer") + urlTransfer.SetCertificatesFile("common:/certs/ca-bundle.crt") + urlTransfer.InitClientCertificates() + urlTransfer.SetUrl(url) + urlTransfer.SetRequest("POST") + urlTransfer.AddHeader("Content-Type", "application/json") + urlTransfer.AddHeader("Authorization", m.sdkKey) + + response = urlTransfer.PostFromString(FormatJson(requestBody)) + + if response = invalid + print "Error sending events" + else + print "Events sent successfully: "; response + end if + end for + + m.taskNode.flush = false + end sub, + getConfig: function() as Object + sdkKey = m.sdkKey + url = m.createSDKConfigUrl(sdkKey, m.user, m.options) + + if url = invalid + return invalid + end if + + ' Create and setup the URL transfer object + urlTransfer = CreateObject("roUrlTransfer") + urlTransfer.SetCertificatesFile("common:/certs/ca-bundle.crt") + urlTransfer.InitClientCertificates() + urlTransfer.SetUrl(url) + + ' Make the API request + response = urlTransfer.GetToString() + + if response <> invalid + ' Parse the JSON response + jsonResponse = ParseJson(response) + if jsonResponse <> invalid + ' Save the response in the config property + m.configEtag = jsonResponse.etag + if m.configEtag = jsonResponse.etag AND FormatJson(m.config) = FormatJson(jsonResponse) + return m.config + end if + m.config = jsonResponse + m.taskNode.config = jsonResponse + m.updateConfigData(jsonResponse) + else + print "Error parsing JSON response" + end if + else + print "Error fetching data from API" + end if + return m.config + end function, + createSDKConfigUrl: function(sdkKey as String, user as Object, options as Object) as String + if sdkKey = invalid + return invalid + end if + + url = getSDKConfigUrl(user, sdkKey, options) + + if url = invalid + return invalid + end if + + return url + end function, + updateConfigData: sub(newConfig as Object) + if newConfig <> invalid + m.taskNode.variables = newConfig.variables + m.taskNode.features = newConfig.features + end if + end sub + } + } + return DevCycleClientObject +end function diff --git a/components/DevCycle/DevCycleOptions.brs b/components/DevCycle/DevCycleOptions.brs new file mode 100644 index 0000000..6f8fe1c --- /dev/null +++ b/components/DevCycle/DevCycleOptions.brs @@ -0,0 +1,50 @@ +function DevCycleOptions(options as Object) as Object + defaultOptions = {} + + defaultOptions["flushEventsIntervalMs"] = 10000 + defaultOptions["disableCustomEventLogging"] = false + defaultOptions["disableAutomaticEventLogging"] = false + defaultOptions["enableEdgeDB"] = false + defaultOptions["apiProxyURL"] = invalid + defaultOptions["eventsApiProxyURL"] = invalid + + populatedOptions = {} + + if options.flushEventsIntervalMs <> invalid + populatedOptions["flushEventsIntervalMs"] = options.flushEventsIntervalMs + else + populatedOptions["flushEventsIntervalMs"] = defaultOptions["flushEventsIntervalMs"] + end if + + if options.disableCustomEventLogging <> invalid + populatedOptions["disableCustomEventLogging"] = options.disableCustomEventLogging + else + populatedOptions["disableCustomEventLogging"] = defaultOptions["disableCustomEventLogging"] + end if + + if options.disableAutomaticEventLogging <> invalid + populatedOptions["disableAutomaticEventLogging"] = options.disableAutomaticEventLogging + else + populatedOptions["disableAutomaticEventLogging"] = defaultOptions["disableAutomaticEventLogging"] + end if + + if options.enableEdgeDB <> invalid + populatedOptions["enableEdgeDB"] = options.enableEdgeDB + else + populatedOptions["enableEdgeDB"] = defaultOptions["enableEdgeDB"] + end if + + if options.apiProxyURL <> invalid + populatedOptions["apiProxyURL"] = options.apiProxyURL + else + populatedOptions["apiProxyURL"] = defaultOptions["apiProxyURL"] + end if + + if options.eventsApiProxyURL <> invalid + populatedOptions["eventsApiProxyURL"] = options.eventsApiProxyURL + else + populatedOptions["eventsApiProxyURL"] = defaultOptions["eventsApiProxyURL"] + end if + + return populatedOptions +end function diff --git a/components/DevCycle/DevCycleSGClient.brs b/components/DevCycle/DevCycleSGClient.brs new file mode 100644 index 0000000..d165a3e --- /dev/null +++ b/components/DevCycle/DevCycleSGClient.brs @@ -0,0 +1,135 @@ +sub InitializeDevCycleClient(sdkKey as String, user as Object, options as Object, taskNode as Dynamic) + taskNode.sdkKey = sdkKey + taskNode.user = user + taskNode.options = options + taskNode.config = invalid + taskNode.variables = invalid + taskNode.features = invalid +end sub + +function getRokuTypeForDefault(value as dynamic) as String + if type(value) = "String" + return "roString" + else if type(value) = "Integer" + return "roInt" + else if type(value) = "Float" + return "roFloat" + else if type(value) = "Boolean" + return "roBoolean" + else if type(value) = "roAssociativeArray" + return "roAssociativeArray" + else + return "unknown" + end if +end function + +function getTypeFromRokuType(value as dynamic) as String + if type(value) = "roString" + return "String" + else if type(value) = "roInt" or type(value) = "roLong" or type(value) = "roFloat" or type(value) = "roInteger" + return "Number" + else if type(value) = "roBoolean" + return "Boolean" + else if type(value) = "roAssociativeArray" + return "JSON" + else + return "unknown" + end if +end function + +function getEvaluatedEvent(variable as Object, defaulted as Boolean) + variableEventType = "variableDefaulted" + if NOT defaulted + variableEventType = "variableEvaluated" + end if + + event = { + type: variableEventType, + target: variable.key, + value: 1, + metaData: { + value: variable.value, + type: getTypeFromRokuType(variable.value) + } + } + if NOT defaulted + event.metaData._variable = variable._id + end if + return event +end function + +function DevCycleSGClient(taskNode as Object) as Object + DevCycleSGClientObject = { + identifyUser: sub(user as Object) + m.private.taskNode.user = user + m.private.taskNode.identifyUser = true + end sub, + + getAllVariables: function() as Object + if NOT m.private.taskNode.initialized + print "Error: DevCycleClient not initialized" + return invalid + end if + return m.private.taskNode.variables + end function, + + getAllFeatures: function() as Object + if NOT m.private.taskNode.initialized + print "Error: DevCycleClient not initialized" + return invalid + end if + return m.private.taskNode.features + end function, + + resetUser: function() as Object + m.private.taskNode.resetUser = true + return m.private.config + end function, + + track: sub(event as Object) + m.private.taskNode.track = event + end sub, + getVariable: function(key as String, default as dynamic) as Object + variable = m.private.taskNode.config.variables[key] + defaulted = false + + if variable = invalid + variable = { + key: key, + value: default, + type: Type(default) + } + defaulted = true + end if + + m.private.taskNode.track = getEvaluatedEvent(variable, defaulted) + return variable + end function, + getVariableValue: function(key as String, default as dynamic) as Object + variable = m.private.taskNode.config.variables[key] + defaulted = false + + if variable = invalid + variable = { + key: key, + value: default, + type: Type(default) + } + defaulted = true + else + if type(variable.value) <> getRokuTypeForDefault(default) + variable.value = default + defaulted = true + end if + end if + + m.private.taskNode.track = getEvaluatedEvent(variable, defaulted) + return variable.value + end function, + private: { + taskNode: taskNode + } + } + + return DevCycleSGClientObject +end function diff --git a/components/DevCycle/DevCycleTask.brs b/components/DevCycle/DevCycleTask.brs new file mode 100644 index 0000000..440aef9 --- /dev/null +++ b/components/DevCycle/DevCycleTask.brs @@ -0,0 +1,49 @@ +sub Init() + m.messagePort = createObject("roMessagePort") + m.top.observeField("sdkKey", "startThread") + m.top.observeField("config", m.messagePort) + m.top.observeField("options", m.messagePort) + m.top.observeField("flush", m.messagePort) + m.top.observeField("track", m.messagePort) + m.top.observeField("identifyUser", m.messagePort) + m.top.observeField("resetUser", m.messagePort) + m.top.initialized = false +end sub + +sub startThread() + m.top.functionName = "mainThread" + m.top.control = "RUN" +end sub + +sub mainThread() + m.client = DevCycleClient(m.top) + m.client.initialize() + m.top.initialized = m.client.private.initialized + while true + ' Use a non-blocking wait with a timeout of 100ms + msg = wait(1000, m.messagePort) + ' Process the message if there is one + if msg <> invalid and type(msg) = "roSGNodeEvent" + field = msg.getField() + if field = "track" + m.client.track(msg.getData()) + else if field = "flush" + m.client.flush() + else if field = "identifyUser" + if m.top.identifyUser = true + m.client.identifyUser(m.top.user) + m.top.identifyUser = false + end if + else if field = "resetUser" + if msg.getData() = true + m.client.resetUser() + m.top.resetUser = false + end if + end if + end if + ' Check the flush timer + if m.client.private.flushTimer.TotalMilliseconds() > m.client.private.flushInterval + m.client.flush() + end if + end while +end sub diff --git a/components/DevCycle/DevCycleTask.xml b/components/DevCycle/DevCycleTask.xml new file mode 100644 index 0000000..aca1130 --- /dev/null +++ b/components/DevCycle/DevCycleTask.xml @@ -0,0 +1,23 @@ + + +