Skip to content

Repository files navigation

sentry-lolcode

Important

Prototype hackathon code. Built in a few hours for a Sentry hackweek. This is not an official Sentry SDK, is not affiliated with or supported by Sentry, and is nowhere near production-ready. Do not point it at anything you care about.

Stylized LOLCODE logo in Sentry style

A working Sentry SDK in LOLCODE. It parses a DSN, generates real event ids, builds a Sentry envelope by hand, and POSTs it over a raw TCP socket.

It is the Sentry SDK for a language that has no exceptions to catch and cannot tell you what time it is. Both of those turn out to be fine.

HAI1.3I IZSENTRY_INITYR"http:://testkey@127.0.0.1::3002/42"MKAYI IZSENTRY_CAPTURE_MESSAGEYR"HAI WORLD"MKAYI IZSENTRY_CAPTURE_EXCEPTIONYR"SyntaxError"ANYR"unexpected KTHXBYE"MKAYKTHXBYE

Install the interpreter

brew install lolcode will not work. It installs lci 0.10.5, which has no SOCKS library — it cannot even tokenize CAN HAS SOCKS?. No sockets, no SDK.

You need the future branch of lci, which is where the socket bindings live. There is a script:

./install-lci.sh

That clones and builds into ./.lci/ (gitignored) and leaves the interpreter at ./.lci/bin/lci, which lolsentry finds on its own. Pass a path to install somewhere else: ./install-lci.sh ~/.local/lci.

If you would rather do it by hand, the one non-obvious part is the cmake flag:

git clone --branch future --depth 1 https://github.com/justinmeza/lci.git
cd lci && mkdir build &&cd build
cmake -DCMAKE_POLICY_VERSION_MINIMUM=3.5 ..
make

-DCMAKE_POLICY_VERSION_MINIMUM=3.5 is mandatory. lci's CMakeLists.txt declares a cmake_minimum_required from 2010 and modern cmake refuses to configure the project without it. Builds clean on arm64 macOS and gives you lci v0.11.2.

Include the SDK in your program

LOLCODE has no import system, and this is not me being lazy — it is the interpreter:

  • CAN HAS <name>? is a hardcoded four-way branch over STDLIB, STDIO, SOCKS and STRING. There is no search path and no file lookup. An unknown name is a silent no-op: CAN HAS SENTRY? compiles, does nothing, and tells you nothing.
  • lci a.lol b.lol runs each file in its own fresh scope, so functions defined in one file are invisible to the next.

So the include step is concatenation, and lolsentry is the linker: it splices sentry.lol and your program into a single HAI..KTHXBYE and runs it.

./lolsentry your-program.lol

Your program is a normal, complete LOLCODE file with its own HAI 1.3 and KTHXBYElolsentry strips both and re-wraps the pair. It works from any directory and from a symlink on your PATH, so a consumer elsewhere on disk just calls it by path:

/path/to/sentry-lolcode/lolsentry my-app.lol

Use --emit to print the linked program instead of running it, which is the only sane way to debug this. LCI=/path/to/lci overrides the interpreter.

The three calls

I IZSENTRY_INITYR"<dsn>"MKAYI IZSENTRY_CAPTURE_MESSAGEYR"<text>"MKAYI IZSENTRY_CAPTURE_EXCEPTIONYR"<type>"ANYR"<value>"MKAY

SENTRY_INIT parses and stores the DSN and seeds the id generator. Both capture functions send one event and FOUND YR the 32-character event id they generated, so you can hold onto it:

I HAS AeidITZI IZSENTRY_CAPTURE_MESSAGEYR"HAI WORLD"MKAYVISIBLE"sentry took it as :{eid}"

Message events go up at level info, exceptions at level error.

Output: the SDK is silent when things work

On the happy path the SDK prints nothing at all — not on init, not per send. Your stdout is yours. A program that captures two events and prints nothing itself produces zero bytes on stdout and zero bytes on stderr.

Two things do get said, and both go to stderr, because lci has INVISIBLEVISIBLE's stderr twin — so diagnostics never have to pollute stdout:

whengoes togated?
ingest answered with something other than 200, or never answeredstderralways
SENTRY_CAPTURE_* called before SENTRY_INITstderralways

Those two stay on unconditionally: silently dropping an event is worse than a line of noise on stderr.

For the running commentary, set SENTRY_DEBUG:

SENTRY_DEBUGRWIN
O HAI SENTRY: project 42 @ 127.0.0.1:3002
KTHX SENTRY SED 200 FOR adae51dce8c03cbccbaa1c55dbafb2a1

That also goes to stderr. LOLCODE has no getenv, so a variable the host program assigns is the only switch available — and since the linker drops the SDK and your program into one shared scope, assigning SENTRY_DEBUG anywhere in your program is all it takes. There is no SENTRY_INIT overload for it because lci checks function arity strictly, so a second parameter would break every one-argument call.

Write your DSN with ::

In LOLCODE a literal colon inside a yarn is spelled ::. A bare : starts an escape sequence, so the DSN you would write anywhere else:

http://testkey@127.0.0.1:3002/42

is typed out like this:

I IZSENTRY_INITYR"http:://testkey@127.0.0.1::3002/42"MKAY

Get this wrong and it fails in a genuinely great way: :3 is LOLCODE's carriage-return escape, so 127.0.0.1:3002 silently becomes 127.0.0.1, a carriage return, and 002. Ask me how I know.

How it actually reaches Sentry

LOLCODE has no TLS. Not a weak cipher suite — no TLS at all. SOCKS gives you RESOLV, BIND, KONN, PUT and GET over a plain TCP socket, and that is the entire network stack.

So this SDK speaks plain HTTP, and in production you point it at a local Sentry Relay, which accepts the plain HTTP envelope on localhost and forwards it to Sentry over TLS. Relay terminates TLS on the SDK's behalf. That is a documented, supported Sentry deployment pattern, and it is the honest answer here: this SDK never speaks HTTPS itself. Give it an http:// DSN pointing at your Relay.

Against the bundled mock server it talks plain HTTP to 127.0.0.1, same as it would to a Relay.

Run it

Start the mock ingest server in one shell:

PORT=3002 node mock-ingest.js

And in another:

./lolsentry examples/hello.lol
SENT 233cfc6a92907b7ad689aafbb9e93efb AN b86ff693b9003f5297006dc00185218f
SO LONG AN THX FOR ALL THE EVENTS

Both of those lines are the example program's own VISIBLE calls. The SDK itself says nothing — uncomment SENTRY_DEBUG R WIN at the top of examples/hello.lol to hear from it on stderr.

The server side, which validates the auth header, the envelope structure and the event id format, and rejects anything malformed with a 400 and a reason:

POST /api/42/envelope/ (184 bytes)
project=42 key=testkey
client=sentry.lolcode/0.1.0
timestamp omitted, server will stamp on receipt
info: HAI WORLD
ACCEPTED 233cfc6a92907b7ad689aafbb9e93efb
POST /api/42/envelope/ (225 bytes)
project=42 key=testkey
client=sentry.lolcode/0.1.0
timestamp omitted, server will stamp on receipt
error: SyntaxError: unexpected KTHXBYE
ACCEPTED b86ff693b9003f5297006dc00185218f

What goes on the wire

DSN

http://<public_key>@<host>:<port>/<project_id>

Split on //, @, : and /. None of the four fields can contain a colon, which is why splitting on one works.

Event payloads

SENTRY_CAPTURE_MESSAGE YR "HAI WORLD":

{"event_id":"0123456789abcdef0123456789abcdef","platform":"other","level":"info","logentry":{"formatted":"HAI WORLD"}}

SENTRY_CAPTURE_EXCEPTION YR "SyntaxError" AN YR "unexpected KTHXBYE":

{"event_id":"0123456789abcdef0123456789abcdef","platform":"other","level":"error","exception":{"values":[{"type":"SyntaxError","value":"unexpected KTHXBYE"}]}}

event_id is 32 lowercase hex characters, no dashes. There is no timestamp field — see below.

The envelope

Three newline-separated JSON lines plus a trailing newline. The event id appears on line 1 and line 3.

{"event_id":"<id>"}
{"type":"event"}
{"event_id":"<id>","platform":"other","level":"info","logentry":{"formatted":"HAI WORLD"}}

The request

POST /api/<project_id>/envelope/ HTTP/1.1
Host: <host>:<port>
Content-Type: application/x-sentry-envelope
Content-Length: <byte length of body>
X-Sentry-Auth: Sentry sentry_version=7, sentry_key=<public_key>, sentry_client=sentry.lolcode/0.1.0
Connection: close
<the three lines above>

Content-Length is a byte count, not a character count. STRING'Z LEN is strlen under the hood and PUT sends strlen bytes, so the two agree.

A successful send returns 200 with {"id":"<event_id>"}.


Notes from the basement

There is no clock. At all.

lci's entire standard library is four modules: STDLIB (MIX, BLOW), STDIO (OPEN, DIAF, LUK, SCRIBBEL, AGEIN, CLOSE), SOCKS (RESOLV, BIND, LISTN, KONN, CLOSE, PUT, GET) and STRING (LEN, AT). That is 17 functions and not one of them reads a clock. There is no time, no date, no monotonic counter, nothing.

So this SDK omits timestamp entirely and lets Sentry stamp the event when it arrives, which is a documented, supported thing for an SDK to do. The alternative would be inventing a number, and a fabricated timestamp is worse than no timestamp.

For completeness: lci does have I DUZ "<command>", which is popen, so I DUZ "date" would get you a clock. That is /bin/date's clock read through a subprocess, not LOLCODE telling the time, and shelling out to a coreutil to timestamp an event is not a thing an SDK should do quietly. So we don't.

There are no exceptions either.

SENTRY_CAPTURE_EXCEPTION takes a type and a value as two plain yarns because there is nothing else to take. LOLCODE has no throw, no catch, and no error value of any kind — WTF?/OMG/OMGWTF looks like exception handling but is just a switch statement. The closest thing in any library is STDIO'Z DIAF, which is ferror() and hands back a bare TROOF: no type, no message, no stack.

So there is nothing to unwrap and no stack to walk, and there is no stacktrace on these events. Two yarns the caller types out is the honest signature. Faking frames would be lying to your own issue stream.

The colon is cursed

lci resolves escape sequences once per cast, and :: greedily consumes the colon that would have started the next escape. Since every ":" in JSON becomes :" in LOLCODE source, writing a colon as :: right before one mis-lexes — and ::{ breaks interpolation the same way.

The fix is to keep a single colon in a variable and interpolate it:

I HAS ASENTRY_COLONITZ"::"I HAS AevtITZ"{:"level:":{SENTRY_COLON}:"info:"}"

No literal in sentry.lol spells a colon any other way.

SMOOSH eats its own accumulator

SMOOSH casts every operand, and casting resolves escapes — so acc R SMOOSH acc AN ch MKAY in a loop re-resolves the accumulator on every single pass. Anything built up character by character has to be a fixed point of that resolution: plain bytes only, no escape sequences, or it degrades a little more each iteration.

That is why the JSON escaper emits a colon as the JSON escape \u003a rather than trying to keep a real one alive, and why every string in sentry.lol that needs to survive intact is assembled by interpolation (":{a}:{b}", which resolves each piece exactly once and drops it in without rescanning) instead of by SMOOSH.

Event ids without a random number generator

STDLIB's MIX/BLOW are srand/rand, and with no clock there is nothing to seed them with. Seeding off /dev/urandom does not rescue it either, because the only number LOLCODE can extract from a byte is a string length: LUK stops at the first NUL, so a one-byte read has length 1 unless that byte happened to be NUL or a colon. Measured over 12 runs, a quarter of them gathered zero entropy and produced a byte-identical event id — which Sentry would treat as a duplicate and drop.

So this SDK does not use the PRNG at all. It reads bytes off /dev/urandom and keeps only the ones that already are hex digits, comparing each byte against all 16 because there is no ord() to convert one into a number:

IM IN YRsiftinUPPIN YRjTILBOTH SAEMjAN16hexchRI IZSTRING'ZATYRSENTRY_HEXANYRjMKAYBOTH SAEMbyteANhexchO RLY?YA RLYeidRSMOOSHeidANhexchMKAYgotRSUMOFgotAN1GTFOOICIM OUTTA YRsiftin

Uniform bytes in, uniform hex out, at the cost of throwing away 15 of every 16 reads — about 500 one-byte reads per event id. Rejection sampling is the whole trick, and it means ids are drawn straight from the kernel CSPRNG rather than from a badly seeded rand().

Reading /dev/urandom in larger blocks segfaults the interpreter on binary data, so: one byte at a time.

Files

sentry.lolthe SDK
lolsentrythe linker — splices sentry.lol into your program
install-lci.shbuilds the future branch of lci into ./.lci
examples/hello.lolsends one message and one exception
mock-ingest.jslocal ingest server that validates and prints what it got

Limitations

  • No TLS, so no direct-to-Sentry. Point it at a local Relay.
  • No timestamp and no stacktrace, for the reasons above.
  • SENTRY_CAPTURE_* blocks until the server answers, and GET has no timeout — if nothing is listening it waits forever.
  • Message text is escaped for ", \, :, newline, tab and carriage return. Other control characters are passed through as-is.
  • One event per connection: BIND, KONN, PUT, GET, CLOSE, every time.
  • Diagnostics are INVISIBLE lines on stderr. LOLCODE has no logger to route them through, so there are no levels and no way to redirect them in-process.

About

Prototype hackweek Sentry SDK for LOLCODE — not an official Sentry SDK

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages