· 5 years ago · Feb 04, 2020, 10:56 PM
1--[[
2 Raven-RBXLua by nomer888
3 August 22nd, 2017
4
5 Remember to enable HttpService.
6
7
8 This module should only be used on the server to prevent clients spamming requests
9 to your Sentry project and potentially risking your account/project being blacklisted.
10
11 Clients should send errors to the server, which the server will then report to Sentry.
12 This functionality is built into this module. To use it, call
13 RavenClient:ConnectRemoteEvent(remoteEvent) with a RemoteEvent not in use by anything
14 else other than clients.
15
16 For client > server error reporting, clients may only report exceptions (with or
17 without traceback), with a limit number of events (default 5) per player per server.
18 This module will attempt to group and anonymize data to reduce unique event generation.
19
20 If players try to send spoofed data, they will no longer be able to report any
21 errors at all in that server.
22
23
24
25 API:
26
27 Raven:Client(string dsn[, table config])
28 Creates a new Raven client used to send events.
29
30 @dsn: the DSN located in your Sentry project. Do not share this.
31 (https://sentry.io/<user>/<project>/settings/keys/)
32
33 @config: a table of attributes applied to all events before being sent to Sentry.
34 See list of relevant attributes.
35
36
37
38 Raven:SendMessage(string message[, string level = Raven.EventLevel.Info][, table config])
39 Sends plain message event to Sentry.
40
41 @level: a string describing the severity level of the event
42 Valid levels (in Raven.EventLevel):
43 Fatal
44 Error
45 Warning
46 Info
47 Debug
48
49 @config: a table of attributes applied to this event before being sent to Sentry.
50 Overrides default attributes of client, if set.
51 See list of relevant attributes.
52
53
54
55 Raven:SendException(string ExceptionType, string errorMessage[, <string, table> traceback][, table config])
56 Send exception event to Sentry.
57
58 @ExceptionType: a string describing the type of exception.
59 Provided exception types (in Raven.ExceptionType):
60 Server (for errors on the server)
61 Client (for errors on the client)
62
63 @errorMessage: a string describing the error. Typically the second argument returned from pcall
64 or an error message from LogService.
65
66 @traceback: a string returned by debug.traceback() OR a premade stacktrace, used to add stacktrace
67 information to the event.
68
69 @config: a table of attributes applied to this event before being sent to Sentry.
70 Overrides default attributes of client, if set.
71 See list of relevant attributes.
72
73
74
75 List of relevant attributes:
76 (from https://docs.sentry.io/clientdev/attributes/#attributes)
77
78 logger
79 The name of the logger which created the record.
80
81 level
82 The record severity.
83 Enumeration of valid levels in Raven.EventLevel
84
85 culprit
86 The name of the transaction (or culprit) which caused this exception.
87
88 release
89 The release version of the application.
90
91 tags
92 A hash array of tags for this event.
93 Merges with client's list of tags, if set.
94
95 environment
96 The environment name, such as ‘production’ or ‘staging’.
97
98 extra
99 An arbitrary mapping of additional metadata to store with the event.
100
101 message
102 Human-readable message to store with the event.
103
104
105
106 Example, server:
107
108 local raven = require(script.Raven)
109 local client = raven:Client("DSN here")
110
111 client:ConnectRemoteEvent(Instance.new("RemoteEvent", game.ReplicatedStorage))
112
113 local success, err = pcall(function() error("test server error") end)
114 if (not success) then
115 client:SendException(raven.ExceptionType.Server, err, debug.traceback())
116 end
117
118 client:SendMessage("Fatal error", raven.EventLevel.Fatal)
119 client:SendMessage("Basic error", raven.EventLevel.Error)
120 client:SendMessage("Warning message", raven.EventLevel.Warning)
121 client:SendMessage("Info message", raven.EventLevel.Info)
122 client:SendMessage("Debug message", raven.EventLevel.Debug)
123
124 local LogService = game:GetService("LogService")
125 LogService.MessageOut:Connect(function(message, messageType)
126 if (messageType == Enum.MessageType.MessageError) then
127 client:SendException(raven.ExceptionType.Server, message)
128 end
129 end
130
131
132
133 Example, client:
134
135 local success, err = pcall(function() error("test client error") end)
136 if (not success) then
137 game.ReplicatedStorage.RemoteEvent:FireServer(err, debug.traceback())
138 end
139--]]
140local logWarnings = true
141local maxClientErrorCount = 5
142
143local Http = game:GetService("HttpService")
144
145local GenerateUUID
146do
147 local hexTable = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f'}
148 local rand = Random.new()
149
150 local function RandomHex(length)
151 local s = ""
152 for i = 1, length do
153 s = s .. hexTable[rand:NextInteger(1, 16)]
154 end
155 return s
156 end
157
158 GenerateUUID = function()
159 return string.format("%s4%s8%s%s", RandomHex(12), RandomHex(3), RandomHex(3), RandomHex(12))
160 end
161end
162
163local function GetTimestamp()
164 local t = os.date("!*t")
165 return ("%04d-%02d-%02dT%02d:%02d:%02d"):format(t.year, t.month, t.day, t.hour, t.min, t.sec)
166end
167
168local function TrySend(client, rawData, headers)
169 if (client.enabled) then
170 local succeed, err = pcall(Http.JSONEncode, Http, rawData)
171 if (succeed) then
172 local packetJSON = err
173 local succeed, err = pcall(Http.PostAsync, Http, client.requestUrl, packetJSON, Enum.HttpContentType.ApplicationJson, true, headers)
174 if (succeed) then
175 local responseJSON = err
176 local succeed, err = pcall(Http.JSONDecode, Http, responseJSON)
177 if (succeed) then
178 return true, err
179 else
180 return false, err
181 end
182 else
183 local status = tonumber(err:match("^HTTP (%d+)"))
184 if (status) then
185 if (status >= 400 and status < 500) then
186 if (logWarnings) then
187 warn(("Raven: HTTP %d in TrySend, JSON packet:"):format(status))
188 warn(packetJSON)
189 warn("Headers:")
190 for i, v in pairs(headers) do
191 warn(i.." "..v)
192 end
193 warn("Response:")
194 warn(err)
195 if (status == 401) then
196 warn("Please check the validity of your DSN.")
197 end
198 end
199 elseif (status == 429) then
200 if (logWarnings) then
201 warn("Raven: HTTP 429 Retry-After in TrySend, disabling SDK for this server.")
202 end
203 client.enabled = false
204 end
205 end
206 return false, err
207 end
208 else
209 return false, err
210 end
211 else
212 return false, "SDK disabled."
213 end
214end
215
216local sentryVersion = "7"
217local sdkName = "raven-rbxlua"
218local sdkVersion = "1.0"
219
220local function SendEvent(client, packet, config)
221 assert(type(packet) == "table")
222
223 local timestamp = GetTimestamp()
224
225 packet.event_id = GenerateUUID()
226 packet.timestamp = timestamp
227 packet.logger = "server"
228 packet.platform = "other"
229 packet.sdk = {
230 name = sdkName;
231 version = sdkVersion;
232 }
233
234 for i, v in pairs(client.config) do
235 packet[i] = v
236 end
237
238 for i, v in pairs(config) do
239 if (i == "tags" and type(packet[i]) == "table") then
240 for k, c in pairs(v) do
241 packet[i][k] = c
242 end
243 else
244 packet[i] = v
245 end
246 end
247
248 local headers = {
249 Authorization = client.authHeader:format(timestamp)
250 }
251
252 local succeed, response = TrySend(client, packet, headers)
253 return succeed, response
254end
255
256local function StringTraceToTable(trace)
257 local stacktrace = {}
258
259 for line in trace:gmatch("[^\n\r]+") do
260 if (not line:match("^Stack Begin$") and not line:match("^Stack End$")) then
261 local path, lineNum, value = line:match("^Script '(.-)', Line (%d+)%s?%-?%s?(.*)$")
262 if (path and lineNum and value) then
263 stacktrace[#stacktrace + 1] = {
264 filename = path;
265 ["function"] = value or "nil";
266 lineno = lineNum;
267 }
268 else
269 return false, "invalid traceback"
270 end
271 end
272 end
273
274 if (#stacktrace == 0) then
275 return false, "invalid traceback"
276 end
277
278 local sorted = {}
279 for i = #stacktrace, 1, -1 do
280 sorted[i] = stacktrace[i]
281 end
282
283 return true, sorted
284end
285
286local Raven = {}
287
288Raven.EventLevel = {
289 Fatal = "fatal";
290 Error = "error";
291 Warning = "warning";
292 Info = "info";
293 Debug = "debug";
294}
295
296Raven.ExceptionType = {
297 Server = "ServerError";
298 Client = "ClientError";
299}
300
301function Raven:Client(dsn, config)
302 local client = {}
303
304 client.DSN = dsn
305
306 local protocol,
307 publicKey,
308 secretKey,
309 host,
310 path,
311 projectId = dsn:match("^([^:]+)://([^:]+):([^@]+)@([^/]+)(.*/)(.+)$")
312
313 assert(protocol and protocol:lower():match("^https?$"), "invalid DSN: protocol not valid")
314 assert(publicKey, "invalid DSN: public key not valid")
315 assert(secretKey, "invalid DSN: secret key not valid")
316 assert(host, "invalid DSN: host not valid")
317 assert(path, "invalid DSN: path not valid")
318 assert(projectId, "invalid DSN: project ID not valid")
319
320 client.requestUrl = ("%s://%s%sapi/%d/store/"):format(protocol, host, path, projectId)
321 client.authHeader = ("Sentry sentry_version=%d,sentry_timestamp=%s,sentry_key=%s,sentry_secret=%s,sentry_client=%s"):format(
322 sentryVersion,
323 "%s",
324 publicKey,
325 secretKey,
326 ("%s/%s"):format(sdkName, sdkVersion)
327 )
328
329 client.config = config or {}
330 client.enabled = true
331
332 return setmetatable(client, {__index = self})
333end
334
335function Raven:SendMessage(message, level, config)
336 config = config or {}
337
338 local packet = {
339 level = level or self.EventLevel.Info;
340 message = message;
341 }
342
343 return SendEvent(self, packet, config)
344end
345
346function Raven:SendException(eType, errorMessage, traceback, config)
347 assert(type(eType) == "string", "invalid exception type")
348 config = config or {}
349
350 local exception = {
351 type = eType;
352 value = errorMessage;
353 }
354
355 local culprit
356
357 if (type(traceback) == "string") then
358 local success, frames = StringTraceToTable(traceback)
359 if (success) then
360 exception.stacktrace = {frames = frames}
361 culprit = frames[#frames].filename
362 else
363 if (logWarnings) then
364 warn(("Raven: Failed to convert string traceback to stacktrace: %s"):format(frames))
365 warn(traceback)
366 end
367 end
368 elseif (type(traceback) == "table") then
369 exception.stacktrace = {frames = traceback}
370 culprit = traceback[#traceback].filename
371 end
372
373 local packet = {
374 level = Raven.EventLevel.Error;
375 exception = {exception};
376 culprit = culprit;
377 }
378
379 return SendEvent(self, packet, config)
380end
381
382local function ScrubData(playerName, errorMessage, traceback)
383 errorMessage = errorMessage:gsub(playerName, "<Player>")
384
385 local success, stacktrace
386 if (traceback ~= nil) then
387 success, stacktrace = StringTraceToTable(traceback)
388 if (success) then
389 for i, frame in pairs(stacktrace) do
390 frame.filename = frame.filename:gsub(playerName, "<Player>")
391 end
392 end
393 else
394 success = true
395 end
396
397 if (success and errorMessage ~= "") then
398 return true, errorMessage, stacktrace
399 end
400 return false, "invalid exception"
401end
402
403local errorCount = setmetatable({}, {__mode = "k"})
404
405function Raven:ConnectRemoteEvent(remoteEvent)
406 assert(typeof(remoteEvent) == "Instance" and remoteEvent.ClassName == "RemoteEvent", "ConnectRemoteEvent did not receive RemoteEvent instance")
407
408 remoteEvent.OnServerEvent:Connect(function(player, errorMessage, traceback)
409 local count = errorCount[player]
410 if (not count) then
411 count = maxClientErrorCount
412 end
413
414 if (count > 0) then
415 if (type(errorMessage) == "string" and (type(traceback) == "string" or traceback == nil)) then
416 local success, scrubbedErrorMessage, scrubbedTraceback = ScrubData(player.Name, errorMessage, traceback)
417 if (success) then
418 count = count - 1
419 self:SendException(Raven.ExceptionType.Client, scrubbedErrorMessage, scrubbedTraceback)
420 else
421 if (logWarnings) then
422 warn(("Raven: Player '%s' tried to send spoofed data, their ability to report errors has been disabled."):format(player.Name))
423 warn("errorMessage:")
424 warn(errorMessage)
425 warn("traceback:")
426 warn(traceback)
427 end
428 count = 0
429 end
430 else
431 if (logWarnings) then
432 warn(("Raven: Player '%s' tried to send spoofed data, their ability to report errors has been disabled."):format(player.Name))
433 warn("errorMessage:")
434 warn(errorMessage)
435 warn("traceback:")
436 warn(traceback)
437 end
438 count = 0
439 end
440 end
441
442 errorCount[player] = count
443 end)
444end
445
446return Raven