add gs
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
--[[
|
||||
Specialized analytics reporter for ephemeral counters.
|
||||
Useful for tracking at-a-glance health of a feature.
|
||||
]]
|
||||
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local Diag = {}
|
||||
Diag.__index = Diag
|
||||
|
||||
-- reportingService - (object) any object that defines the same functions for Diag as AnalyticsService
|
||||
function Diag.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, Diag)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function Diag:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- counterName : (string) the name of the ephemeral counter to increment
|
||||
-- amount : (int) the value to increment the counter by
|
||||
function Diag:reportCounter(counterName, amount)
|
||||
assert(type(counterName) == "string", "Expected counterName to be a string")
|
||||
assert(type(amount) == "number", "Expected amount to be a number")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- use special naming convention for Xbox counters
|
||||
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
|
||||
-- executes the scripts in the wrong authorization level
|
||||
local platformID = Enum.Platform.None
|
||||
pcall(function()
|
||||
platformID = UserInputService:GetPlatform()
|
||||
end)
|
||||
if platformID == Enum.Platform.XBoxOne then
|
||||
counterName = "Xbox-" .. tostring(counterName)
|
||||
end
|
||||
|
||||
-- the AnalyticsService should automatically handle batch reporting
|
||||
self._reporter:ReportCounter(counterName, amount)
|
||||
end
|
||||
|
||||
|
||||
-- category : (string) the name of the statistics buffer which to append the data
|
||||
-- value : (number) any type of numeric value
|
||||
function Diag:reportStats(category, value)
|
||||
assert(type(category) == "string", "Expected category to be a string")
|
||||
assert(type(value) == "number", "Expected value to be a number")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:ReportStats(category, value)
|
||||
end
|
||||
|
||||
return Diag
|
||||
@@ -0,0 +1,176 @@
|
||||
return function()
|
||||
local Diag = require(script.Parent.Diag)
|
||||
|
||||
local testCounterName = "testCounter"
|
||||
local testCounterAmount = 1
|
||||
local testCategoryName = "testCategory"
|
||||
local testCategoryValue = 98
|
||||
|
||||
local badTestCounterName = 5
|
||||
local badTestCounterAmount = "hello"
|
||||
local badTestCategoryName = {}
|
||||
local badTestCategoryValue = {}
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:ReportCounter(counterName, amount)
|
||||
assert(counterName == testCounterName, "Unexpected value for counterName: " .. counterName)
|
||||
assert(amount == testCounterAmount, "Unexpected value for amount: " .. amount)
|
||||
end
|
||||
function DebugReportingService:ReportStats(categoryName, value)
|
||||
assert(categoryName == testCategoryName, "Unexpected value for category: " .. categoryName)
|
||||
if value then
|
||||
assert(value == testCategoryValue, "Unexpected value for value: " .. value)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(diag).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should throw an error to be constructed without a Reporting Service", function()
|
||||
expect(function()
|
||||
Diag.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:setEnabled(false)
|
||||
diag:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:setEnabled(false)
|
||||
expect(function()
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("reportCounter()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
diag:setEnabled(false)
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end).to.throw()
|
||||
|
||||
diag:setEnabled(true)
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:reportCounter(testCounterName, testCounterAmount)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the counter name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(badTestCounterName, testCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the amount", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(testCounterName, badTestCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with completely invalid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(badTestCounterName, badTestCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a counter name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(nil, testCounterAmount)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an amount", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(testCounterName, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing any input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportCounter(nil, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("reportStats()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
diag:setEnabled(false)
|
||||
diag:reportStats(testCategoryName, testCategoryValue)
|
||||
end).to.throw()
|
||||
|
||||
diag:setEnabled(true)
|
||||
diag:reportStats(testCategoryName, testCategoryValue)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
diag:reportStats(testCategoryName, testCategoryValue)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the category name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(badTestCategoryName, testCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the value", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(testCategoryName, badTestCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with completely invalid input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(badTestCategoryName, badTestCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a category name", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(nil, testCategoryValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a value", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(testCategoryName, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing any input", function()
|
||||
local diag = Diag.new(DebugReportingService)
|
||||
expect(function()
|
||||
diag:reportStats(nil, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,154 @@
|
||||
--[[
|
||||
Specialized reporter for RBX Event Ingest data.
|
||||
Useful for tracking explicit user interactions with screens and guis.
|
||||
]]
|
||||
|
||||
local UserInputService = game:GetService("UserInputService")
|
||||
|
||||
local function getPlatformTarget()
|
||||
local platformTarget = "unknownLua"
|
||||
local platformEnum = Enum.Platform.None
|
||||
|
||||
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
|
||||
-- executes the scripts in the wrong authorization level
|
||||
pcall(function()
|
||||
platformEnum = UserInputService:GetPlatform()
|
||||
end)
|
||||
|
||||
-- bucket the platform based on consumer platform
|
||||
local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX)
|
||||
|
||||
local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android)
|
||||
isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP)
|
||||
|
||||
local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne)
|
||||
isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4)
|
||||
isConsole = isConsole or (platformEnum == Enum.Platform.WiiU)
|
||||
|
||||
-- assign a target based on the form factor
|
||||
if isDesktopClient then
|
||||
platformTarget = "client"
|
||||
elseif isMobileClient then
|
||||
platformTarget = "mobile"
|
||||
elseif isConsole then
|
||||
platformTarget = "console"
|
||||
else
|
||||
-- if we don't have a name for the form factor, report it here so that we can eventually track it down
|
||||
platformTarget = platformTarget .. tostring(platformEnum)
|
||||
end
|
||||
|
||||
|
||||
return platformTarget
|
||||
end
|
||||
|
||||
|
||||
local EventStream = {}
|
||||
EventStream.__index = EventStream
|
||||
|
||||
-- reportingService - (object) any object that defines the same functions for Event Stream as AnalyticsService
|
||||
function EventStream.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, EventStream)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function EventStream:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) table for additional information to appear in the event stream.
|
||||
function EventStream:setRBXEvent(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- This function fires reports to the server right away
|
||||
self._reporter:SetRBXEvent(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
|
||||
function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- this function sends reports to the server in batches, not real-time
|
||||
self._reporter:SetRBXEventStream(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
function EventStream:releaseRBXEventStream()
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:ReleaseRBXEventStream(getPlatformTarget())
|
||||
end
|
||||
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
|
||||
function EventStream:sendEventDeferred(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- this function sends reports to the server in batches, not real-time
|
||||
self._reporter:SendEventDeferred(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
-- eventContext : (string) the location or context in which the event is occurring.
|
||||
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
|
||||
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
|
||||
function EventStream:sendEventImmediately(eventContext, eventName, additionalArgs)
|
||||
local target = getPlatformTarget()
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(eventContext) == "string", "Expected eventContext to be a string")
|
||||
assert(type(eventName) == "string", "Expected eventName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
-- this function sends reports to the server in batches, not real-time
|
||||
self._reporter:SendEventImmediately(target, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
-- additionalArgs : (optional, map<string, string>) table for extra keys to appear in the event stream.
|
||||
function EventStream:updateHeartbeatObject(additionalArgs)
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:UpdateHeartbeatObject(additionalArgs)
|
||||
end
|
||||
|
||||
|
||||
return EventStream
|
||||
+393
@@ -0,0 +1,393 @@
|
||||
return function()
|
||||
|
||||
local EventStream = require(script.Parent.EventStream)
|
||||
|
||||
local testArgs = {
|
||||
testKey = "testValue"
|
||||
}
|
||||
local testContext = "testContext"
|
||||
local testEvent = "testEventName"
|
||||
local badTestArgs = "hello"
|
||||
local badTestContext = {}
|
||||
local badTestEvent = {}
|
||||
|
||||
|
||||
local function isTableEqual(table1, table2)
|
||||
if table1 == table2 then
|
||||
return true
|
||||
end
|
||||
|
||||
if type(table1) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
if type(table2) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
for key, _ in pairs(table1) do
|
||||
if table1[key] ~= table2[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, _ in pairs(table2) do
|
||||
if table2[key] ~= table1[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function createDebugReportingService()
|
||||
local function validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
assert(eventTarget, "no value found for eventTarget")
|
||||
assert(eventContext == testContext, "unexpected value for eventContext : " .. eventContext)
|
||||
assert(eventName == testEvent, "unexpected value for eventName : " .. eventName)
|
||||
if additionalArgs and not isTableEqual(additionalArgs, {}) then
|
||||
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
|
||||
end
|
||||
end
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:SetRBXEvent(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:SetRBXEventStream(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:SendEventImmediately(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:SendEventDeferred(eventTarget, eventContext, eventName, additionalArgs)
|
||||
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
|
||||
end
|
||||
function DebugReportingService:ReleaseRBXEventStream(eventTarget)
|
||||
assert(eventTarget, "no value found for eventTarget")
|
||||
end
|
||||
function DebugReportingService:UpdateHeartbeatObject(additionalArgs)
|
||||
if additionalArgs and not isTableEqual(additionalArgs, {}) then
|
||||
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
|
||||
end
|
||||
end
|
||||
|
||||
return DebugReportingService
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(es).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should not allow construction without a Reporting Service", function()
|
||||
expect(function()
|
||||
EventStream.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local reporter = EventStream.new(createDebugReportingService())
|
||||
reporter:setEnabled(false)
|
||||
reporter:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local reporter = EventStream.new(createDebugReportingService())
|
||||
reporter:setEnabled(false)
|
||||
expect(function()
|
||||
reporter:updateHeartbeatObject()
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setRBXEvent()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEvent(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:setRBXEvent(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:setRBXEvent(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEvent(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEvent(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setRBXEventStream()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEventStream(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:setRBXEventStream(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:setRBXEventStream(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:setRBXEventStream(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:setRBXEventStream(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("sendEventImmediately()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventImmediately(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should call without a fuss when enabled and throw when disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:sendEventImmediately(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:sendEventImmediately(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventImmediately(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventImmediately(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("sendEventDeferred()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventDeferred(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should call without a fuss when enabled and throw when disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:sendEventDeferred(testContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:sendEventDeferred(testContext, testEvent, testArgs)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(nil, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing an event name", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(testContext, nil, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:sendEventDeferred(testContext, testEvent, nil)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a context", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(badTestContext, testEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for a event", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(testContext, badTestEvent, testArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given bad input for additionalArgs", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:sendEventDeferred(testContext, testEvent, badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("releaseRBXEventStream()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:releaseRBXEventStream()
|
||||
end)
|
||||
|
||||
it("should throw when disabled and succeed when enabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:releaseRBXEventStream()
|
||||
end).to.throw()
|
||||
|
||||
es:setEnabled(true)
|
||||
es:releaseRBXEventStream()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("updateHeartbeatObject()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(false)
|
||||
es:updateHeartbeatObject(testArgs)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
es:setEnabled(true)
|
||||
es:updateHeartbeatObject(testArgs)
|
||||
end).never.to.throw()
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:updateHeartbeatObject(testArgs)
|
||||
end)
|
||||
|
||||
it("should succeed even if there aren't any additional args", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
es:updateHeartbeatObject(nil)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input", function()
|
||||
local es = EventStream.new(createDebugReportingService())
|
||||
expect(function()
|
||||
es:updateHeartbeatObject(badTestArgs)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
--[[
|
||||
Specialized reporter for sending data to GA.
|
||||
Useful for creating a breadcrumb trail of user interactions.
|
||||
|
||||
Events in GA are aggregated and organized in order by category, action, label.
|
||||
]]
|
||||
|
||||
local GoogleAnalytics = {}
|
||||
GoogleAnalytics.__index = GoogleAnalytics
|
||||
|
||||
-- reportingService : (table or userdata) any object that defines the same functions for GA as AnalyticsService
|
||||
function GoogleAnalytics.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, GoogleAnalytics)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function GoogleAnalytics:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- category : (string) the most generic category by which to organize data, ex) LuaApp, Errors, GameSettings, etc.
|
||||
-- action : (string) a specific event to record, ex) ButtonPressed, GameExit
|
||||
-- label : (string, optional) a detail to differentiate one action over another, ex) LoginButton, Exit Code 0
|
||||
-- value : (integer, optional) the number of times this event has occurred
|
||||
function GoogleAnalytics:trackEvent(category, action, label, value)
|
||||
assert(type(category) == "string", "Expected category to be a string")
|
||||
assert(type(action) == "string", "Expected action to be a string")
|
||||
if label then
|
||||
assert(type(label) == "string", "Expected label to be a string")
|
||||
end
|
||||
if value then
|
||||
assert(type(value) == "number", "Expected value to be a number")
|
||||
assert(value >= 0, "Expected value must not be a negative value")
|
||||
end
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:TrackEvent(category, action, label, value)
|
||||
end
|
||||
|
||||
|
||||
return GoogleAnalytics
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
return function()
|
||||
local GoogleAnalytics = require(script.Parent.GoogleAnalytics)
|
||||
|
||||
local testCategory = "testCategory"
|
||||
local testAction = "testAction"
|
||||
local testLabel = "testLabel"
|
||||
local testValue = 6
|
||||
local badTestCategory = 13141
|
||||
local badTestAction = {}
|
||||
local badTestLabel = {}
|
||||
local badTestValue = "heyo"
|
||||
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:TrackEvent(category, action, label, value)
|
||||
if category ~= testCategory then
|
||||
error("unexpected value for category: " .. category)
|
||||
end
|
||||
if action ~= testAction then
|
||||
error("unexpected value for action: " .. action)
|
||||
end
|
||||
if label then
|
||||
if label ~= testLabel then
|
||||
error("unexpected value for label: " .. label)
|
||||
end
|
||||
end
|
||||
if value then
|
||||
if value ~= testValue then
|
||||
error("unexpected value for value: " .. value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service and Logging Service", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(ga).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fail to be constructed without a Reporting Service", function()
|
||||
expect(function()
|
||||
GoogleAnalytics.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local reporter = GoogleAnalytics.new(DebugReportingService)
|
||||
reporter:setEnabled(false)
|
||||
reporter:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local reporter = GoogleAnalytics.new(DebugReportingService)
|
||||
reporter:setEnabled(false)
|
||||
expect(function()
|
||||
reporter:trackEvent(testCategory, testAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("trackEvent()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
ga:setEnabled(false)
|
||||
ga:trackEvent(testCategory, testAction, testLabel)
|
||||
end).to.throw()
|
||||
|
||||
ga:setEnabled(true)
|
||||
ga:trackEvent(testCategory, testAction, testLabel)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
ga:trackEvent(testCategory, testAction, testLabel, testValue)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a category", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(nil, testAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a testAction", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, nil, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should not throw an error if it is missing a label", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
ga:trackEvent(testCategory, testAction, nil, testValue)
|
||||
end)
|
||||
|
||||
it("should not throw an error if it is missing a value", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
ga:trackEvent(testCategory, testAction, testLabel)
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for category", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(badTestCategory, testAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for action", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, badTestAction, testLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for label", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, testAction, badTestLabel, testValue)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is given invalid input for value", function()
|
||||
local ga = GoogleAnalytics.new(DebugReportingService)
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, testAction, testLabel, badTestValue)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
ga:trackEvent(testCategory, testAction, testLabel, -1)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
--[[
|
||||
Specialized reporter for sending data to InfluxDb.
|
||||
Useful for very detailed information about specific errors.
|
||||
|
||||
Due to how Influx sends data, it is disallowed on XBox.
|
||||
~Kyler Mulherin (9/12/2017)
|
||||
]]
|
||||
|
||||
local Influx = {}
|
||||
Influx.__index = Influx
|
||||
|
||||
-- reportingService - (object) any object that defines the same functions for Influx as AnalyticsService
|
||||
function Influx.new(reportingService)
|
||||
local rsType = type(reportingService)
|
||||
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
|
||||
|
||||
local self = {
|
||||
_reporter = reportingService,
|
||||
_isEnabled = true,
|
||||
}
|
||||
setmetatable(self, Influx)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
-- isEnabled : (boolean)
|
||||
function Influx:setEnabled(isEnabled)
|
||||
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
|
||||
self._isEnabled = isEnabled
|
||||
end
|
||||
|
||||
-- seriesName : (string) the name of the series as it will appear in InfluxDb
|
||||
-- additionalArgs : (map<string, string>) extra key/values to appear in each series
|
||||
-- throttlingPercent : (int) the chance to actually report this series
|
||||
function Influx:reportSeries(seriesName, additionalArgs, throttlingPercent)
|
||||
additionalArgs = additionalArgs or {}
|
||||
|
||||
assert(type(seriesName) == "string", "Expected seriesName to be a string")
|
||||
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
|
||||
assert(type(throttlingPercent) == "number", "Expected throttlingPercent to be a number")
|
||||
assert(throttlingPercent >= 0 and throttlingPercent <= 10000, "throttlingPercent must be between 0 - 10,000")
|
||||
assert(self._isEnabled, "This reporting service is disabled")
|
||||
|
||||
self._reporter:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercent)
|
||||
end
|
||||
|
||||
return Influx
|
||||
@@ -0,0 +1,165 @@
|
||||
return function()
|
||||
local Influx = require(script.Parent.Influx)
|
||||
|
||||
local testSeriesName = "testSeries"
|
||||
local testArgs = {
|
||||
testKey = "testValue"
|
||||
}
|
||||
local testThrottlingPercentage = 1000
|
||||
|
||||
local badTestSeriesName = 114
|
||||
local badTestArgs = "someString"
|
||||
local badThrottlingPercentage1 = -15
|
||||
local badThrottlingPercentage2 = 150000
|
||||
local badThrottlingPercentage3 = "a lot"
|
||||
|
||||
|
||||
local function isTableEqual(table1, table2)
|
||||
if table1 == table2 then
|
||||
return true
|
||||
end
|
||||
|
||||
if type(table2) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
for key, _ in pairs(table1) do
|
||||
if table1[key] ~= table2[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
for key, _ in pairs(table2) do
|
||||
if table2[key] ~= table1[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
local DebugReportingService = {}
|
||||
function DebugReportingService:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercentage)
|
||||
if seriesName ~= seriesName then
|
||||
error("Unexpected value for seriesName: " .. seriesName)
|
||||
end
|
||||
if throttlingPercentage ~= testThrottlingPercentage then
|
||||
error("Unexpected value for throttlingPercentage: " .. throttlingPercentage)
|
||||
end
|
||||
if isTableEqual(additionalArgs, {}) == false then
|
||||
if isTableEqual(additionalArgs, testArgs) == false then
|
||||
error("Unexpected value for additionalArgs")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe("new()", function()
|
||||
it("should construct with a Reporting Service", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(influx).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fail to be constructed without a Reporting Service", function()
|
||||
expect(function()
|
||||
Influx.new(nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("setEnabled()", function()
|
||||
it("should succeed with valid input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:setEnabled(false)
|
||||
influx:setEnabled(true)
|
||||
end)
|
||||
it("should disable the reporter", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:setEnabled(false)
|
||||
expect(function()
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("reportSeries()", function()
|
||||
it("should work when appropriately enabled / disabled", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
|
||||
expect(function()
|
||||
influx:setEnabled(false)
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
|
||||
|
||||
influx:setEnabled(true)
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end)
|
||||
|
||||
it("should succeed with valid input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
|
||||
end)
|
||||
|
||||
it("should succeed even if it is missing any additionalArgs", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, nil, testThrottlingPercentage)
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the seriesName", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(badTestSeriesName, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the throttlingPercentage - out of range - below zero", function()
|
||||
expect(function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage1)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the throttlingPercentage - out of range - above cap", function()
|
||||
expect(function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage2)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with invalid input for the throttlingPercentage - bad type", function()
|
||||
expect(function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage3)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error with completely invalid input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(badTestSeriesName, badTestArgs, badThrottlingPercentage1)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a seriesName", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(nil, testArgs, testThrottlingPercentage)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing a throttlingPercentage", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(testSeriesName, testArgs, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if it is missing any input", function()
|
||||
local influx = Influx.new(DebugReportingService)
|
||||
expect(function()
|
||||
influx:reportSeries(nil, nil, nil)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user