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.
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"MKAYKTHXBYEbrew 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.shThat 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.
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 overSTDLIB,STDIO,SOCKSandSTRING. 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.lolruns 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.lolYour program is a normal, complete LOLCODE file with its own HAI 1.3 and
KTHXBYE — lolsentry 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.lolUse --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.
I IZSENTRY_INITYR"<dsn>"MKAYI IZSENTRY_CAPTURE_MESSAGEYR"<text>"MKAYI IZSENTRY_CAPTURE_EXCEPTIONYR"<type>"ANYR"<value>"MKAYSENTRY_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.
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 INVISIBLE —
VISIBLE's stderr twin — so diagnostics never have to pollute stdout:
| when | goes to | gated? |
|---|---|---|
ingest answered with something other than 200, or never answered | stderr | always |
SENTRY_CAPTURE_* called before SENTRY_INIT | stderr | always |
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_DEBUGRWINO 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.
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"MKAYGet 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.
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.
Start the mock ingest server in one shell:
PORT=3002 node mock-ingest.jsAnd in another:
./lolsentry examples/hello.lolSENT 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
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.
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.
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"}}
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>"}.
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.
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.
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 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.
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 YRsiftinUniform 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.
sentry.lol | the SDK |
lolsentry | the linker — splices sentry.lol into your program |
install-lci.sh | builds the future branch of lci into ./.lci |
examples/hello.lol | sends one message and one exception |
mock-ingest.js | local ingest server that validates and prints what it got |
- No TLS, so no direct-to-Sentry. Point it at a local Relay.
- No
timestampand nostacktrace, for the reasons above. SENTRY_CAPTURE_*blocks until the server answers, andGEThas 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
INVISIBLElines on stderr. LOLCODE has no logger to route them through, so there are no levels and no way to redirect them in-process.
