add gs
This commit is contained in:
+287
@@ -0,0 +1,287 @@
|
||||
--[[
|
||||
Specialized reporter for sending data to Backtrace.
|
||||
Useful for reporting Lua errors.
|
||||
]]
|
||||
|
||||
local CorePackages = game:GetService("CorePackages")
|
||||
|
||||
local Cryo = require(CorePackages.Cryo)
|
||||
local t = require(CorePackages.Packages.t)
|
||||
|
||||
local BacktraceReport = require(script.Parent.BacktraceReport)
|
||||
local ErrorQueue = require(script.Parent.Parent.ErrorQueue)
|
||||
|
||||
local DEVELOPMENT_IN_STUDIO = game:GetService("RunService"):IsStudio()
|
||||
|
||||
local DEFAULT_LOG_INTERVAL = 60 -- seconds
|
||||
|
||||
local BacktraceReporter = {}
|
||||
BacktraceReporter.__index = BacktraceReporter
|
||||
|
||||
local IBacktraceReporter = t.strictInterface({
|
||||
httpService = t.some(t.instanceOf("HttpService"), t.interface({
|
||||
JSONEncode = t.callback,
|
||||
JSONDecode = t.callback,
|
||||
RequestInternal = t.callback,
|
||||
})),
|
||||
token = t.string,
|
||||
processErrorReportMethod = t.optional(t.callback),
|
||||
queueOptions = t.optional(t.table),
|
||||
generateLogMethod = t.optional(t.callback),
|
||||
logIntervalInSeconds = t.optional(t.numberPositive),
|
||||
})
|
||||
|
||||
function BacktraceReporter.new(arguments)
|
||||
local valid, message = IBacktraceReporter(arguments)
|
||||
local self
|
||||
|
||||
if valid then
|
||||
self = {
|
||||
_isEnabled = true,
|
||||
_httpService = arguments.httpService,
|
||||
_errorQueue = nil,
|
||||
_reportUrl = game:GetFastString("ErrorUploadToBacktraceBaseUrl") .. "token=" .. arguments.token,
|
||||
_processErrorReportMethod = arguments.processErrorReportMethod,
|
||||
|
||||
_sharedAttributes = {},
|
||||
_sharedAnnotations = {},
|
||||
_generateLogMethod = arguments.generateLogMethod,
|
||||
_logIntervalInSeconds = arguments.logIntervalInSeconds or DEFAULT_LOG_INTERVAL,
|
||||
_lastLogTime = 0,
|
||||
}
|
||||
elseif (DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__) then
|
||||
error("invalid arguments for BacktraceReporter: " .. message)
|
||||
else
|
||||
self = {
|
||||
_isEnabled = false,
|
||||
}
|
||||
end
|
||||
|
||||
setmetatable(self, BacktraceReporter)
|
||||
|
||||
-- Create and start the ErrorQueue for deferred reports.
|
||||
if self._isEnabled then
|
||||
self._errorQueue = ErrorQueue.new(function(...)
|
||||
self:_reportErrorFromErrorQueue(...)
|
||||
end, arguments.queueOptions)
|
||||
|
||||
self._errorQueue:startTimer()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function BacktraceReporter:sendErrorReport(report, log)
|
||||
if not self._isEnabled then
|
||||
return
|
||||
end
|
||||
|
||||
-- Validating the report can be slow;
|
||||
-- And an invalid report might still be able to be processed, sent, and accepted by Backtrace.
|
||||
-- So we don't validate reports in production.
|
||||
if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then
|
||||
assert(report:validate())
|
||||
end
|
||||
|
||||
local encodeSuccess, jsonData = pcall(function()
|
||||
return self._httpService:JSONEncode(report)
|
||||
end)
|
||||
|
||||
if not encodeSuccess then
|
||||
warn("Cannot convert report to Json")
|
||||
return
|
||||
end
|
||||
|
||||
pcall(function()
|
||||
local httpRequest = self._httpService:RequestInternal({
|
||||
Url = self._reportUrl .. "&format=json",
|
||||
Method = "POST",
|
||||
Headers = {
|
||||
["Content-Type"] = "application/json",
|
||||
},
|
||||
Body = jsonData,
|
||||
})
|
||||
|
||||
httpRequest:Start(function(success, response)
|
||||
-- Be aware that even when a response is 200, the report
|
||||
-- might still be rejected/deleted by Backtrace after it is received.
|
||||
if response.StatusCode == 200 and
|
||||
log ~= nil then
|
||||
|
||||
local decodeSuccesss, decodedBody = pcall(function()
|
||||
return self._httpService:JSONDecode(response.Body)
|
||||
end)
|
||||
|
||||
if decodeSuccesss and decodedBody._rxid ~= nil then
|
||||
self:_sendLogToReport(decodedBody._rxid, log)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function BacktraceReporter:_sendLogToReport(reportRxid, log)
|
||||
if type(log) ~= "string" or #log == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
pcall(function()
|
||||
local httpRequest = self._httpService:RequestInternal({
|
||||
Url = self._reportUrl .. "&object=" .. reportRxid .. "&attachment_name=log.txt",
|
||||
Method = "POST",
|
||||
Headers = {
|
||||
["Content-Type"] = "text/plain",
|
||||
},
|
||||
Body = log,
|
||||
})
|
||||
|
||||
httpRequest:Start(function(reqSuccess, response)
|
||||
-- We have no use for the result of this request right now.
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function BacktraceReporter:_generateLog()
|
||||
if self._generateLogMethod ~= nil and
|
||||
tick() - self._lastLogTime > self._logIntervalInSeconds then
|
||||
self._lastLogTime = tick()
|
||||
|
||||
local success, log = pcall(function()
|
||||
return self._generateLogMethod()
|
||||
end)
|
||||
|
||||
if success and type(log) == "string" and #log > 0 then
|
||||
return log
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function BacktraceReporter:_generateErrorReport(errorMessage, errorStack, details)
|
||||
local report = BacktraceReport.fromMessageAndStack(errorMessage, errorStack)
|
||||
|
||||
report:addAttributes(self._sharedAttributes)
|
||||
report:addAnnotations(self._sharedAnnotations)
|
||||
|
||||
if type(details) == "string" and details ~= "" then
|
||||
report:addAnnotations({
|
||||
["stackDetails"] = details,
|
||||
})
|
||||
end
|
||||
|
||||
return report
|
||||
end
|
||||
|
||||
-- Immediate reports
|
||||
-- You most likely should not use this. Use reportErrorDeferred instead.
|
||||
function BacktraceReporter:reportErrorImmediately(errorMessage, errorStack, details)
|
||||
if not self._isEnabled then
|
||||
return
|
||||
end
|
||||
|
||||
local newReport = self:_generateErrorReport(errorMessage, errorStack, details)
|
||||
|
||||
if self._processErrorReportMethod ~= nil then
|
||||
newReport = self._processErrorReportMethod(newReport)
|
||||
end
|
||||
|
||||
local log = self:_generateLog()
|
||||
|
||||
self:sendErrorReport(newReport, log)
|
||||
end
|
||||
|
||||
-- Deferred reports using an error queue
|
||||
function BacktraceReporter:reportErrorDeferred(errorMessage, errorStack, details)
|
||||
if not self._isEnabled then
|
||||
return
|
||||
end
|
||||
|
||||
local errorKey = string.format("%s | %s", errorMessage, errorStack)
|
||||
local errorData = {}
|
||||
|
||||
-- If this error is a new one, we want a full report on it.
|
||||
-- Similar errors following this one will be squashed in the queue and share report with this one
|
||||
-- before they're flushed out and reported.
|
||||
if not self._errorQueue:hasError(errorKey) then
|
||||
local newReport = self:_generateErrorReport(errorMessage, errorStack, details)
|
||||
|
||||
if self._processErrorReportMethod ~= nil then
|
||||
newReport = self._processErrorReportMethod(newReport)
|
||||
end
|
||||
|
||||
errorData = {
|
||||
backtraceReport = newReport,
|
||||
log = self:_generateLog(),
|
||||
}
|
||||
end
|
||||
|
||||
self._errorQueue:addError(errorKey, errorData)
|
||||
end
|
||||
|
||||
function BacktraceReporter:_reportErrorFromErrorQueue(errorKey, errorData, errorCount)
|
||||
local errorReport = errorData.backtraceReport
|
||||
local log = errorData.log
|
||||
|
||||
errorReport:addAttributes({
|
||||
ErrorCount = errorCount,
|
||||
})
|
||||
|
||||
self:sendErrorReport(errorReport, log)
|
||||
end
|
||||
|
||||
-- API for updating shared attributes/annotations
|
||||
local IAttributes = BacktraceReport.IAttributes
|
||||
|
||||
function BacktraceReporter:updateSharedAttributes(newAttributes)
|
||||
-- Merge with current one first. This allows usage of Cryo.None.
|
||||
local mergedAttributes = Cryo.Dictionary.join(self._sharedAttributes, newAttributes)
|
||||
|
||||
-- Validate the merged result, and only update if it's valid.
|
||||
local valid, message = IAttributes(mergedAttributes)
|
||||
if not valid then
|
||||
if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then
|
||||
assert(valid, message)
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
self._sharedAttributes = mergedAttributes
|
||||
end
|
||||
|
||||
local IAnnotations = BacktraceReport.IAnnotations
|
||||
|
||||
function BacktraceReporter:updateSharedAnnotations(newAnnotations)
|
||||
-- Although annotations can be nested tables, this is not a recursive merge.
|
||||
local mergedAnnotations = Cryo.Dictionary.join(self._sharedAnnotations, newAnnotations)
|
||||
|
||||
local valid, message = IAnnotations(mergedAnnotations)
|
||||
if not valid then
|
||||
if DEVELOPMENT_IN_STUDIO or _G.__TESTEZ_RUNNING_TEST__ then
|
||||
assert(valid, message)
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
self._sharedAnnotations = mergedAnnotations
|
||||
end
|
||||
|
||||
-- Flush all reports in the queue.
|
||||
function BacktraceReporter:reportAllErrors()
|
||||
if self._errorQueue ~= nil then
|
||||
self._errorQueue:reportAllErrors()
|
||||
end
|
||||
end
|
||||
|
||||
function BacktraceReporter:stop()
|
||||
self._isEnabled = false
|
||||
|
||||
if self._errorQueue ~= nil then
|
||||
self:reportAllErrors()
|
||||
self._errorQueue:stopTimer()
|
||||
end
|
||||
end
|
||||
|
||||
return BacktraceReporter
|
||||
Reference in New Issue
Block a user