Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8a2596a
logging sentry init opts
zhiyan114 Dec 16, 2025
d59f0fa
LogAttribute Type
zhiyan114 Dec 16, 2025
6699e31
Add 'trace' level for logging
zhiyan114 Dec 16, 2025
f312d49
Fix LogAttribute type
zhiyan114 Dec 17, 2025
bbf3699
Finish interactable function calls
zhiyan114 Dec 17, 2025
686dbad
Implement beforeSend method
zhiyan114 Dec 17, 2025
3649a52
Number type check for attribute
zhiyan114 Dec 17, 2025
5d7ae61
Fixed bad condition
zhiyan114 Dec 17, 2025
4e7dc33
Added timer system for log flush algo
zhiyan114 Dec 17, 2025
41ca0f7
Partial log flush implementation
zhiyan114 Dec 17, 2025
0f082bf
Prevent localScript from invoking flushLogs
zhiyan114 Dec 17, 2025
c176ab5
Fix some issues
zhiyan114 Dec 17, 2025
091ff7b
Fix logger:fmt stuff
zhiyan114 Dec 17, 2025
73e5990
Attach logger info to attribute
zhiyan114 Dec 27, 2025
4ad5224
Implement scope stuff
zhiyan114 Dec 27, 2025
569ae1b
Finished with scope attr
zhiyan114 Dec 27, 2025
e4e779f
Fix potential RC during log flushing process
zhiyan114 Dec 27, 2025
abe894d
Wrap user/ctx attr with valToAttrVal
zhiyan114 Dec 27, 2025
84a9fea
Add 'sent_at' to envelope header
zhiyan114 Jan 2, 2026
18c2f5f
Updated LogService Integration
zhiyan114 Jan 2, 2026
f3ebf12
attributes param should accept nil
zhiyan114 Jan 2, 2026
26c268d
Fix stuff lol
zhiyan114 Jan 2, 2026
e8ca94f
Wacky workaround to support client logging
zhiyan114 Jan 2, 2026
fdd6eb3
Fixed few issue with client logging
zhiyan114 Jan 2, 2026
494fbd9
Fixed beforeSendLog not properly patched in SentryClient
zhiyan114 Jan 2, 2026
586b389
Prevent infinite recursive when print/warn used inside beforeSendLog
zhiyan114 Jan 13, 2026
8b88f6f
Replace getfenv with warning message in doc + Added other doc stuff
zhiyan114 Mar 27, 2026
387cf34
Corrected variable case-sensitivity to comply with code-style
zhiyan114 Apr 19, 2026
780d4ff
Fixed case styling for utils
zhiyan114 Apr 19, 2026
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
37 changes: 35 additions & 2 deletions src/Defaults.luau
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ type Filter<T> = (T, Hint) -> (T?)

.Release string?
.Environment string?

.EnableLogs boolean? -- Whether to enable sentry's new structured logging feature
.BeforeSendLog ((log: StructLog) -> StructLog | nil)? -- Enables log content transform/removal before sending it to ingest.
-- WARNING: Do not use print/warn function inside beforeSendLog as it may conflict with LogServiceMessageOut.

.SampleRate number?

Expand Down Expand Up @@ -53,6 +57,11 @@ export type Options = {
BeforeSend: Filter<unknown>?,
-- BeforeSendTransaction: Filter<unknown>?,
BeforeBreadcrumb: Filter<unknown>?,

-- Logging stuff
EnableLogs: boolean?,
BeforeSendLog: ((log: StructLog) -> StructLog | nil)?,


Transport: unknown,
ShutdownTimeout: number?,
Expand Down Expand Up @@ -82,15 +91,18 @@ Module.Options = {
InAppExclude = {},

WithLocals = true,

EnableLogs = false,

Transport = require(script.Parent:WaitForChild("Transport")),
ShutdownTimeout = 2,
}:: Options

Module.Levels = {"fatal", "error", "warning", "info", "debug"}
Module.Levels = {"fatal", "error", "warning", "info", "debug", "trace"}

export type ValidJSONValues = string | number | boolean
export type Level = "fatal" | "error" | "warning" | "info" | "debug"
-- "trace" is only supported for structured logging
export type Level = "fatal" | "error" | "warning" | "info" | "debug" | "trace"
export type Event = {
event_id: string?,
timestamp: string | number | nil,
Expand Down Expand Up @@ -166,6 +178,27 @@ export type Event = {
}}?,
}

-- https://develop.sentry.dev/sdk/telemetry/logs/#default-attributes
export type StructLog = {
timestamp: number; -- Unix Epoch (in sec)
trace_id: string; -- Random 16 byte hex
level: Level;
body: string; -- Formatted Message
attributes: {
["sentry.environment"]: string?;
["sentry.release"]: string?;
["sentry.sdk.name"]: string?;
["sentry.sdk.version"]: string?;
["user.id"]: string?;
["user.name"]: string?;
} | {[string]: any};
};
export type LogParamFormat = {
template: string;
params: {string}
}



--// Functions

Expand Down
18 changes: 17 additions & 1 deletion src/Hub/Client.luau
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,30 @@ end
The client is disabled after this method is called.
]=]
function Client:Close(Timeout: number?)

Transport:FlushLogs()
end

--[=[
Same as close difference is that the client is NOT disposed after invocation.
]=]
function Client:Flush(Timeout: number?)
Transport:FlushLogs()
end

--[=[
Handles incoming events from hub and allow custom levels/scopes if needed
]=]
function Client:CaptureLog(LogPayload: Defaults.StructLog, Scope)
LogPayload["trace_id"] = string.gsub(HttpService:GenerateGUID(false), "-", "");
if self.SDK_INTERFACE then
LogPayload.attributes["sentry.sdk.name"] = self.SDK_INTERFACE.name;
LogPayload.attributes["sentry.sdk.version"] = self.SDK_INTERFACE.version;
end


-- @TODO: Apply scope attributes afterward when that is done lol...
return Transport:CaptureLog(LogPayload)

end

export type Client = typeof(Client.new())
Expand Down
17 changes: 17 additions & 0 deletions src/Hub/Scope.luau
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function Scope.new()
extra = {},
contexts = {},
tags = {},
attributes = {},

_event_processors = {},
}, Scope)
Expand Down Expand Up @@ -151,6 +152,22 @@ function Scope:SetFingerprint(Fingerprint: {string})
self.fingerprint = Fingerprint
end

--[=[
]=]
function Scope:SetAttributes(attributes: {string})
for k,v in pairs(attributes) do
self.attributes[k] = v
end
end

--[=[
]=]
function Scope:RemoveAttribute(attribute: string)
if(self.attributes[attribute]) then
self.attributes[attribute] = nil;
end
end


--[=[
@param Processor (Event, Hint) -> (Event)
Expand Down
122 changes: 121 additions & 1 deletion src/Hub/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ local Defaults = require(script.Parent:WaitForChild("Defaults"))
local ClientClass = require(script:WaitForChild("Client"))
local ScopeClass = require(script:WaitForChild("Scope"))

local RunService = game:GetService("RunService")
local Utils = require(script.Parent.Utils)

--[=[
@class Hub

Expand All @@ -16,6 +19,7 @@ local ScopeClass = require(script:WaitForChild("Scope"))
known as async local or context local).
]=]
local Hub = {}
local Logger = {}

--// Functions

Expand All @@ -24,7 +28,9 @@ local Hub = {}
@param Scope Scope
]=]
function Hub.new(Client: ClientClass.Client?, Scope: ScopeClass.Scope?)
return setmetatable({Client = Client or ClientClass.new(), Scope = Scope or ScopeClass.new()}, {__index = Hub})
local MainTab = setmetatable({Client = Client or ClientClass.new(), Scope = Scope or ScopeClass.new()}, {__index = Hub})
MainTab.Logger = setmetatable({Hub=MainTab}, {__index = Logger})
return MainTab
end


Expand Down Expand Up @@ -68,6 +74,120 @@ function Hub:CaptureMessage(Message: string, Level: Defaults.Level? )
})
end


--[=[
@param body string or Logger.fmt content
@param attribute any key/value (string, number, or bool)
]=]
function Logger:Debug(Body: string | Defaults.LogParamFormat, Attribute: {[string]: any}?)
return self:CaptureLog(Body, "debug", Attribute);
end

--[=[
@param body string or Logger.fmt content
@param attribute any key/value (string, number, or bool)
]=]
function Logger:Info(Body: string | Defaults.LogParamFormat, Attribute: {[string]: any}?)
return self:CaptureLog(Body, "info", Attribute);
end

--[=[
@param body string or Logger.fmt content
@param attribute any key/value (string, number, or bool)
]=]
function Logger:Warn(Body: string | Defaults.LogParamFormat, Attribute: {[string]: any}?)
return self:CaptureLog(Body, "warn", Attribute);
end

--[=[
@param body string or Logger.fmt content
@param attribute any key/value (string, number, or bool)
]=]
function Logger:Error(Body: string | Defaults.LogParamFormat, Attribute: {[string]: any}?)
return self:CaptureLog(Body, "error", Attribute);
end

--[=[
@param template 'string.format' text template
@param ... 'string.format' parameters to complete the template
]=]
function Logger:fmt(Template: string, ...: string): Defaults.LogParamFormat
local content = {}
content.template = Template
content.params = {...}
return content;
end

--[=[
@param body string or Logger.fmt content
@param attributes custom log metadata (key/value)
@param level level
]=]
function Logger:CaptureLog(Body: string | Defaults.LogParamFormat, Level: Defaults.Level, Attributes: {[string]: any}?)
if not Body then return; end
-- Byapss for client since it doesn't have init options
if not (self.Hub.Options and self.Hub.Options.EnableLogs) and not RunService:IsClient() then return; end
local LogPayload = {
timestamp = DateTime.now().UnixTimestamp;
level = Level or "info"; -- set default level if needed
body = Body;
attributes = {
["sentry.environment"] = self.Hub.Options and self.Hub.Options.Environment;
["sentry.release"] = self.Hub.Options and self.Hub.Options.Release;
};
}

-- Convert Formatted body to payload
if typeof(LogPayload.body) == "table" then
LogPayload.body = string.format(Body.template, table.unpack(Body.params));
LogPayload.attributes["sentry.message.template"] = Utils.ValToAttrVal(Body.template)
for i,v in ipairs(Body.params) do
LogPayload.attributes["sentry.message.parameter."..i-1] = Utils.ValToAttrVal(v)
end
end

-- Copy over attribute table
if Attributes then
for k,v in pairs(Attributes) do
LogPayload.attributes[k] = Utils.ValToAttrVal(v)
end
end

-- Copy over user attributes
if self.Hub.Scope.user then
local userObj = Utils.FlattenTable(self.Hub.Scope.user, "user")
for k,v in pairs(userObj) do
LogPayload.attributes[k] = Utils.ValToAttrVal(v);
end
end

-- Server/Client ctx
if self.Hub.Scope.Logger then
LogPayload.attributes.Logger = Utils.ValToAttrVal(self.Hub.Scope.Logger);
end

-- Copy over Scope Attributes
if(self.Hub.Scope.attributes) then
for k,v in pairs(self.Hub.Scope.attributes) do
LogPayload.attributes[k] = Utils.ValToAttrVal(v)
end
end

self.Hub:_CaptureLog(LogPayload)
end

--[=[
This is internal method. Please use the Hub.Logger table to send sentry formatted log
or customize LogServiceMessageOut integration instead
]=]
function Hub:_CaptureLog(payload)
if self.Options and self.Options.BeforeSendLog then
local newPayload = self.Options.BeforeSendLog(payload);
if newPayload == nil then return; end
return self.Client:CaptureLog(newPayload, self.Scope)
end
return self.Client:CaptureLog(payload, self.Scope)
end
--[=[
]=]
function Hub:CaptureException(ErrorMessage: string?)
Expand Down
6 changes: 3 additions & 3 deletions src/Integrations/LogServiceMessageOut.luau
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ function Module:SetupOnce(AddGlobalEventProcessor, CurrentHub)
end

if MessageType == Enum.MessageType.MessageWarning then
CurrentHub:CaptureMessage(Message, "warning")
CurrentHub.Logger:Warn(Message, {origin="LogServiceMessageOut"})
-- elseif MessageType == Enum.MessageType.MessageInfo then
-- CurrentHub:CaptureMessage(Message, "info")
-- CurrentHub.Logger:Info(Message, {origin="LogServiceMessageOut"})
-- elseif MessageType == Enum.MessageType.MessageOutput then
-- CurrentHub:CaptureMessage(Message, "debug")
-- CurrentHub.Logger:Debug(Message, {origin="LogServiceMessageOut"})
end
end)
end
Expand Down
9 changes: 8 additions & 1 deletion src/Integrations/SentryClientRelay/SentryClient.server.luau
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
--// Initialization

local PlayerService = game:GetService("Players")
local hubClass = require(script.Parent.Parent.Parent:WaitForChild("Hub"))

local RemoteEvent = script.Parent:WaitForChild("RemoteEvent")
local SentrySDK = require(script.Parent.Parent.Parent)
Expand All @@ -16,8 +17,14 @@ SentrySDK:ConfigureScope(function(Scope)
Scope.logger = "client"
Scope:SetUser(PlayerService.LocalPlayer)
Scope._AddGlobalEventProcessor(function(Event, Hint)
RemoteEvent:FireServer(Event, Hint)
RemoteEvent:FireServer("event", Event, Hint)

return nil
end)

if(not hubClass.Options) then hubClass.Options = {} end
hubClass.Options.beforeSendLog = function(e)
RemoteEvent:FireServer("log", e)
return;
end
end)
15 changes: 12 additions & 3 deletions src/Integrations/SentryClientRelay/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,24 @@ function Module:SetupOnce(AddGlobalEventProcessor, CurrentHub)

SentryClient.Enabled = true
RemoteEvent.Parent = script
RemoteEvent.OnServerEvent:Connect(function(Player, Event, Hint)
RemoteEvent.OnServerEvent:Connect(function(Player, EnvType, Event, Hint)
if not SentryClient.Enabled then
return
end

SentrySDK:GetCurrentHub():Clone():ConfigureScope(function(Scope)
local newHub = SentrySDK:GetCurrentHub():Clone():ConfigureScope(function(Scope)
Scope.logger = "client"
Scope:SetUser(Player)
end):CaptureEvent(Event, Hint)
end)

if(EnvType == "event") then
newHub:CaptureEvent(Event, Hint)
end

if(EnvType == "log") then
-- Event = Client Log Payload
newHub:_CaptureLog(Event)
end
end)
end

Expand Down
Loading