Heya! I’ve run into an interesting problem with a small wrapper I wrote for Roblox’s Playfab API.
I use playfab to log errors that occur in my game. Both the error message and the stack trace are reported to the analytics. As part of this process, I filter certain pieces of information from the strings, including the player’s name. For example, if a stack trace has Players.Noble_Draconian.Modules.TableUtils, line 180
, the username gets scrubbed, resulting in Players.[Player].Modules.TableUtils, line 180
.
This works perfectly fine both in my test environments as well as the production environment. However, for some odd reason, strings that reference platyer’s avatar models under Workspace
don’t get filtered sometimes. If a string has Foobar is not a valid member of Workspace.Noble_Draconian
, it won’t get filtered, and the username shows up in the analytics. Other times, it will get filtered and end up as Foobar is not a valid member of Workspace.[Player]
, as seen below:
I’m at a loss as to how this happens - the string filtering is done serverside, using string.gsub
. It works 100% of the time when I test locally, but it fails to filter randomly in production.
I was hoping an extra set of eyes might be able to spot a problem that I haven’t been able to.
Here’s the code, for reference:
Server
--[[
Handles the various aspects of the game's analytics
--]]
local AnalyticsService = {Client = {}}
AnalyticsService.Client.Server = AnalyticsService
---------------------
-- Roblox Services --
---------------------
local RbxAnalyticsService = game:GetService("AnalyticsService")
local ScriptContextService = game:GetService("ScriptContext")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
-------------
-- Defines --
-------------
local DEBUG_ENABLED = false
local GAME_VERSION = ReplicatedStorage._GameVersion.Value
local ServerErrorCache = {}
local PlayersErrorCache = {}
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Helper functions
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
local function ScrubPlayerFromOutput(Player,OutputString)
return string.gsub(OutputString,Player.Name,"[Player]")
end
local function ScrubBadCharsFromOutput(OutputString)
OutputString = string.gsub(OutputString,"\r","")
OutputString = string.gsub(OutputString,"\n","")
OutputString = string.gsub(OutputString,",","")
OutputString = string.gsub(OutputString,'"',"")
return OutputString
end
local function CleanOutput(Player,OutputString)
if Player ~= nil then
OutputString = ScrubPlayerFromOutput(Player,OutputString)
end
OutputString = ScrubBadCharsFromOutput(OutputString)
return OutputString
end
local function LogErrorToAnalytics(Player,ErrorMessage,ErrorStackTrace)
if (game.GameId == 2799591695 or game.GameId == 2022463223) and not RunService:IsStudio() then
RbxAnalyticsService:FireLogEvent(
Player,
Enum.AnalyticsLogLevel.Error,
ErrorMessage,
{
errorCode = "ScriptContext.Error",
stackTrace = ErrorStackTrace
},
{
GameVersion = GAME_VERSION,
GameId = game.GameId
}
)
end
if DEBUG_ENABLED then
warn("Logged error!")
warn("Message",ErrorMessage)
warn("Stack trace",ErrorStackTrace)
end
end
local function HandlePlayerAdded(Player)
PlayersErrorCache[tostring(Player.UserId)] = {}
end
local function HandlePlayerLeaving(Player)
PlayersErrorCache[tostring(Player.UserId)] = nil
end
local function GetPlayerErrorCache(Player)
return PlayersErrorCache[tostring(Player.UserId)]
end
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- API Methods
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : Client.RequestProcessError
-- @Description : Processes the specified error for the calling player
-- @Params : string "ErrorMessage" - The error message of the error that occured
-- string "StackTrace" - The stack trace of the error that occured
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function AnalyticsService.Client:RequestProcessError(Player,ErrorMessage,StackTrace)
local CleanedErrorMessage = CleanOutput(Player,ErrorMessage)
local CleanedStackTrace = CleanOutput(Player,StackTrace)
local FullErrorString = CleanedErrorMessage .. " | " .. CleanedStackTrace
local PlayerErrorCache = GetPlayerErrorCache(Player)
if table.find(PlayerErrorCache,FullErrorString) == nil then
table.insert(PlayerErrorCache,FullErrorString)
LogErrorToAnalytics(Player,CleanedErrorMessage,CleanedStackTrace)
end
end
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : Init
-- @Description : Called when the service module is first loaded.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function AnalyticsService:Init()
self:DebugLog("[Analytics Service] Initialized!")
end
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : Start
-- @Description : Called after all services are loaded.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function AnalyticsService:Start()
self:DebugLog("[Analytics Service] Started!")
------------------------------------------------------------------
-- Creating/destroying player error caches on player join/leave --
------------------------------------------------------------------
for _,Player in pairs(Players:GetPlayers()) do
coroutine.wrap(HandlePlayerAdded)(Player)
end
Players.PlayerAdded:connect(HandlePlayerAdded)
Players.PlayerRemoving:connect(HandlePlayerLeaving)
---------------------------------------------------
-- Reporting errors to analytics when they occur --
---------------------------------------------------
ScriptContextService.Error:connect(function(ErrorMessage,ErrorStackTrace)
local FullErrorString = ErrorMessage .. " | " .. ErrorStackTrace
if table.find(ServerErrorCache,FullErrorString) == nil then
table.insert(ServerErrorCache,FullErrorString)
LogErrorToAnalytics(nil,CleanOutput(nil,ErrorMessage),CleanOutput(nil,ErrorStackTrace))
end
end)
----------------------------------------
-- Flushing error caches every minute --
----------------------------------------
while true do
task.wait(60)
ServerErrorCache = {}
for Key,_ in pairs(PlayersErrorCache) do
PlayersErrorCache[Key] = {}
end
end
end
return AnalyticsService
Client
--[[
Handles the client-sided aspects of analytics logging
--]]
local AnalyticsController = {}
---------------------
-- Roblox Services --
---------------------
local ScriptContextService = game:GetService("ScriptContext")
------------------
-- Dependencies --
------------------
local AnalyticsService;
-------------
-- Defines --
-------------
local ErrorCache = {}
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : Init
-- @Description : Called when the Controller module is first loaded.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function AnalyticsController:Init()
AnalyticsService = self:GetService("AnalyticsService")
self:DebugLog("[Analytics Controller] Initialized!")
end
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- @Name : Start
-- @Description : Called after all Controllers are loaded.
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
function AnalyticsController:Start()
self:DebugLog("[Analytics Controller] Started!")
------------------------------------------------------------
-- Reporting client-side errors to server when they occur --
------------------------------------------------------------
ScriptContextService.Error:connect(function(ErrorMessage,ErrorStackTrace)
local FullErrorString = ErrorMessage .. " | " .. ErrorStackTrace
if table.find(ErrorCache,FullErrorString) == nil then
table.insert(ErrorCache,FullErrorString)
AnalyticsService:RequestProcessError(ErrorMessage,ErrorStackTrace)
end
end)
---------------------------------------
-- Clearing error cache every minute --
---------------------------------------
while true do
task.wait(60)
ErrorCache = {}
end
end
return AnalyticsController
Thank you in advance!