Skip to content

Commit 4426d9c

Browse files
authored
Merge pull request #26 from metrik-tech/feature/ENG-169
feature/eng 169: Implement client sided error logging security/mitigations.
2 parents 8f82050 + ca6f6ec commit 4426d9c

12 files changed

+216
-36
lines changed

Src/Client.luau

+40
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ local Error = require(script.Parent.Enums.Error)
1111
local ErrorFormats = require(script.Parent.Data.ErrorFormats)
1212

1313
local FlagsController = require(script.Parent.Controllers.FlagsController)
14+
local BreadcrumbController = require(script.Parent.Controllers.BreadcrumbController)
15+
local ContextController = require(script.Parent.Controllers.ContextController)
1416

1517
local ON_INIT_LIFECYCLE_NAME = "OnInit"
1618
local ON_START_LIFECYCLE_NAME = "OnStart"
@@ -34,6 +36,40 @@ function MetrikSDK.Private.FromError(_: MetrikPrivateAPI, errorEnum: string, ...
3436
return string.format(ErrorFormats[errorEnum], ...)
3537
end
3638

39+
function MetrikSDK.Private.AwaitUntilReady(self: MetrikPrivateAPI)
40+
repeat task.wait() until self.IsInitialized
41+
end
42+
43+
--[=[
44+
...
45+
46+
@method CreateBreadcrumb
47+
@within MetrikSDK.Client
48+
49+
@return ()
50+
]=]
51+
--
52+
function MetrikSDK.Public.CreateBreadcrumb(self: MetrikPublicAPI, message: string)
53+
self.Private:AwaitUntilReady()
54+
55+
BreadcrumbController:CreateBreadcrumbFor(debug.info(2, "s"), message)
56+
end
57+
58+
--[=[
59+
...
60+
61+
@method SetContext
62+
@within MetrikSDK.Client
63+
64+
@return ()
65+
]=]
66+
--
67+
function MetrikSDK.Public.SetContext(self: MetrikPublicAPI, context: { [string]: any })
68+
self.Private:AwaitUntilReady()
69+
70+
ContextController:CreateContextFor(debug.info(2, "s"), context)
71+
end
72+
3773
--[=[
3874
...
3975
@@ -44,6 +80,8 @@ end
4480
]=]
4581
--
4682
function MetrikSDK.Public.GetFlag(self: MetrikPublicAPI, flagName: string)
83+
self.Private:AwaitUntilReady()
84+
4785
return FlagsController:EvaluateFlag(flagName)
4886
end
4987

@@ -81,6 +119,8 @@ function MetrikSDK.Public.InitializeAsync(self: MetrikPublicAPI)
81119
table.sort(metrikControllers, function(serviceA, serviceB)
82120
return (serviceA.Priority or 0) > (serviceB.Priority or 0)
83121
end)
122+
123+
repeat task.wait() until script.Parent:GetAttribute("INIT_COMPLETE")
84124

85125
Runtime:CallMethodOn(metrikControllers, ON_INIT_LIFECYCLE_NAME)
86126
Runtime:CallMethodOn(metrikControllers, ON_START_LIFECYCLE_NAME)

Src/Containers/Client.client.luau

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
local Metrik = require(script.Parent.Parent)
22

3-
Metrik.Client:InitializeAsync():andThen(function()
4-
script.Parent:Destroy()
5-
end)
3+
Metrik.Client:InitializeAsync()

Src/Containers/Server.server.luau

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
require(script.Parent.Parent)
2-
3-
return task.defer(script.Destroy, script)
1+
require(script.Parent.Parent)
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
local Console = require(script.Parent.Parent.Packages.Console)
2+
3+
local Network = require(script.Parent.Parent.Network.Client)
4+
5+
local MAX_MESSAGE_LENGTH = 512
6+
local MAX_SOURCE_LENGTH = 256
7+
8+
local BreadcrumbController = { }
9+
10+
BreadcrumbController.Store = { } :: { [Instance]: Breadcrumb }
11+
12+
BreadcrumbController.Priority = 0
13+
BreadcrumbController.Reporter = Console.new(`{script.Name}`)
14+
15+
function BreadcrumbController.CreateBreadcrumbFor(self: BreadcrumbController, sourcePath: string, message: string)
16+
assert(#message < MAX_MESSAGE_LENGTH, `Message is too long (max length is ${MAX_MESSAGE_LENGTH})`)
17+
assert(#sourcePath < MAX_SOURCE_LENGTH, `Source path is too long (max length is ${MAX_SOURCE_LENGTH})`)
18+
19+
Network.CreateBreadcrumb.Fire({
20+
message = message,
21+
sourcePath = sourcePath,
22+
})
23+
end
24+
25+
export type BreadcrumbController = typeof(BreadcrumbController)
26+
export type Breadcrumb = {
27+
timestamp: string,
28+
message: string
29+
}
30+
31+
return BreadcrumbController
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
local HttpService = game:GetService("HttpService")
2+
3+
local Console = require(script.Parent.Parent.Packages.Console)
4+
5+
local Network = require(script.Parent.Parent.Network.Client)
6+
7+
local ContextController = { }
8+
9+
ContextController.Store = { } :: { [Instance]: Context }
10+
11+
ContextController.Priority = 0
12+
ContextController.Reporter = Console.new(`{script.Name}`)
13+
14+
function ContextController.CreateContextFor(self: ContextController, sourcePath: string, context: Context)
15+
Network.CreateContext.Fire({
16+
contextJSON = HttpService:JSONEncode(context),
17+
sourcePath = sourcePath,
18+
})
19+
end
20+
21+
export type ContextController = typeof(ContextController)
22+
export type Context = { [string]: any }
23+
24+
return ContextController

Src/Controllers/LogCaptureControllers.luau

+3-6
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,16 @@ LogCaptureControllers.MessageQueue = {}
1616

1717
LogCaptureControllers.MessageQueueUpdated = Signal.new()
1818

19-
function LogCaptureControllers.OnMessageError(self: LogCaptureControllers, message: string, trace: string, filePath: string)
19+
function LogCaptureControllers.OnMessageError(self: LogCaptureControllers, message: string, trace: string)
2020
table.insert(self.MessageQueue, {
2121
["message"] = message,
22-
["script"] = filePath,
2322
["trace"] = trace,
2423
})
2524
end
2625

2726
function LogCaptureControllers.OnStart(self: LogCaptureControllers)
28-
ScriptContext.Error:Connect(function(message: string, trace: string, script: Instance)
29-
local filePath = script and script:GetFullName() or "?"
30-
31-
self:OnMessageError(message, trace, filePath)
27+
ScriptContext.Error:Connect(function(message: string, trace: string)
28+
self:OnMessageError(message, trace)
3229
end)
3330

3431
task.spawn(function()

Src/Server.luau

+4-2
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ end
8787
]=]
8888
--
8989
function MetrikSDK.Public.CreateBreadcrumb(self: MetrikPublicAPI, message: string)
90-
BreadcrumbService:CreateBreadcrumbFor(self.Private:GetCallingScript(), message)
90+
BreadcrumbService:CreateBreadcrumbFor(nil, self.Private:GetCallingScript(), message)
9191
end
9292

9393
--[=[
@@ -100,7 +100,7 @@ end
100100
]=]
101101
--
102102
function MetrikSDK.Public.SetContext(self: MetrikPublicAPI, context: { [string]: any })
103-
ContextService:CreateContextFor(self.Private:GetCallingScript(), context)
103+
ContextService:CreateContextFor(nil, self.Private:GetCallingScript(), context)
104104
end
105105

106106
--[=[
@@ -188,6 +188,8 @@ function MetrikSDK.Public.InitializeAsync(self: MetrikPublicAPI, settings: {
188188
return reject(response)
189189
end
190190

191+
script.Parent:SetAttribute("INIT_COMPLETE", true)
192+
191193
self.Private.IsInitialized = true
192194

193195
self.Private.Reporter:Debug(`Loaded all MetrikSDK Services ({os.clock() - runtimeClockSnapshot}ms)`)

Src/Services/BreadcrumbService.luau

+28-7
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,37 @@
1+
local Players = game:GetService("Players")
2+
13
local Console = require(script.Parent.Parent.Packages.Console)
24

35
local GetScriptFromFullName = require(script.Parent.Parent.Util.GetScriptFromFullName)
46

7+
local Network = require(script.Parent.Parent.Network.Server)
8+
59
local BreadcrumbService = { }
610

711
BreadcrumbService.Store = { } :: { [Instance]: Breadcrumb }
12+
BreadcrumbService.PlayerStore = { } :: { [Player]: typeof(BreadcrumbService.Store) }
813

914
BreadcrumbService.Priority = 0
1015
BreadcrumbService.Reporter = Console.new(`{script.Name}`)
1116

12-
function BreadcrumbService.GetBreadcrumbsFor(self: BreadcrumbService, sourcePath: string): { Breadcrumb }
17+
function BreadcrumbService.GetBreadcrumbsFor(self: BreadcrumbService, player: Player?, sourcePath: string): { Breadcrumb }
1318
local source = GetScriptFromFullName(sourcePath)
19+
local store = player and self.PlayerStore[player] or self.Store
1420

1521
if not source then
1622
return { }
1723
end
1824

19-
if not self.Store[source] then
25+
if not store[source] then
2026
return { }
2127
end
2228

23-
return self.Store[source]
29+
return store[source]
2430
end
2531

26-
function BreadcrumbService.CreateBreadcrumbFor(self: BreadcrumbService, sourcePath: string, message: string)
32+
function BreadcrumbService.CreateBreadcrumbFor(self: BreadcrumbService, player: Player?, sourcePath: string, message: string)
2733
local source = GetScriptFromFullName(sourcePath)
34+
local store = player and self.PlayerStore[player] or self.Store
2835
local breadcrumbObject = {
2936
message = message,
3037
timestamp = DateTime.now():ToIsoDate()
@@ -34,11 +41,25 @@ function BreadcrumbService.CreateBreadcrumbFor(self: BreadcrumbService, sourcePa
3441
return
3542
end
3643

37-
if not self.Store[source] then
38-
self.Store[source] = { }
44+
if not store[source] then
45+
store[source] = { }
3946
end
4047

41-
table.insert(self.Store[source], breadcrumbObject)
48+
table.insert(store[source], breadcrumbObject)
49+
end
50+
51+
function BreadcrumbService.OnStart(self: BreadcrumbService)
52+
Network.CreateBreadcrumb.SetCallback(function(player: Player, breadcrumb: { message: string, sourcePath: string })
53+
if not self.PlayerStore[player] then
54+
self.PlayerStore[player] = { }
55+
end
56+
57+
self:CreateBreadcrumbFor(player, breadcrumb.sourcePath, breadcrumb.message)
58+
end)
59+
60+
Players.PlayerRemoving:Connect(function(player: Player)
61+
BreadcrumbService.PlayerStore[player] = nil
62+
end)
4263
end
4364

4465
export type BreadcrumbService = typeof(BreadcrumbService)

Src/Services/ContextService.luau

+20-2
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
local HttpService = game:GetService("HttpService")
2+
local Players = game:GetService("Players")
23

34
local Console = require(script.Parent.Parent.Packages.Console)
45

56
local GetScriptFromFullName = require(script.Parent.Parent.Util.GetScriptFromFullName)
67

8+
local Network = require(script.Parent.Parent.Network.Server)
9+
710
local ContextService = { }
811

912
ContextService.Store = { } :: { [Instance]: Context }
13+
ContextService.PlayerStore = { } :: { [Player]: typeof(ContextService.Store) }
1014

1115
ContextService.Priority = 0
1216
ContextService.Reporter = Console.new(`{script.Name}`)
1317

14-
function ContextService.GetContextFor(self: ContextService, sourcePath: string)
18+
function ContextService.GetContextFor(self: ContextService, player: Player?, sourcePath: string)
1519
local source = GetScriptFromFullName(sourcePath)
1620

1721
if not source then
@@ -21,7 +25,7 @@ function ContextService.GetContextFor(self: ContextService, sourcePath: string)
2125
return self.Store[source]
2226
end
2327

24-
function ContextService.CreateContextFor(self: ContextService, sourcePath: string, context: Context)
28+
function ContextService.CreateContextFor(self: ContextService, player: Player?, sourcePath: string, context: Context)
2529
local source = GetScriptFromFullName(sourcePath)
2630

2731
if not source then
@@ -39,6 +43,20 @@ function ContextService.CreateContextFor(self: ContextService, sourcePath: strin
3943
self.Store[source] = context
4044
end
4145

46+
function ContextService.OnStart(self: ContextService)
47+
Network.CreateContext.SetCallback(function(player: Player, context: { contextJSON: string, sourcePath: string })
48+
if not self.PlayerStore[player] then
49+
self.PlayerStore[player] = { }
50+
end
51+
52+
self:CreateContextFor(player, context.sourcePath, HttpService:JSONDecode(context.contextJSON))
53+
end)
54+
55+
Players.PlayerRemoving:Connect(function(player: Player)
56+
ContextService.PlayerStore[player] = nil
57+
end)
58+
end
59+
4260
export type ContextService = typeof(ContextService)
4361
export type Context = { [string]: any }
4462

0 commit comments

Comments
 (0)