TL;DR: The SDK ships with first-party analytics and error reporting โ logEvent, logScreen, trace, and recordError, optionally tied to a real setUserId. Events buffer on the client and flush to your backend, so there's no third-party analytics library to add and the data never leaves your control.
Most apps reach for a hosted analytics SDK on day one, and within a week they're shipping user behaviour to someone else's servers and wiring a second account into the build. LingCode Cloud folds the basics โ events, screen views, timing, and caught errors โ into the same client you already use for data and auth. You call a method, the SDK batches it, and it lands in your own backend. This page covers why that split matters and exactly how to use each call.
Analytics and crash reporting are the two things almost every app needs and almost every app outsources. Outsourcing them means a third dependency in your bundle, another vendor with a copy of your users' behaviour, and one more dashboard to log into. The SDK's telemetry surface removes all three: the events you record are sent to the same backend that holds your data, batched and flushed by the client, with no extra library to install.
The model is deliberately small. You record four kinds of signal โ events, screen views, performance traces, and errors โ and you can optionally attach a real user id so the data segments by who did what. Everything else (buffering, batching, retry-on-flush) is handled for you.
Telemetry is first-party. Events accumulate in a small client-side buffer and are posted to your backend's telemetry endpoint โ they do not transit any third-party analytics service. Because the destination is your own LingCode Cloud backend, the analytics data stays exactly where the rest of your app's data lives.
Everything hangs off client.telemetry. Throughout the samples below the client is named lingcode โ inside /try and the Mac preview that's already window.lingcode; in your own app it's whatever LingCode.createClient(...) returned.
logEvent(name, params?) โ void โ record a named event. params is an optional record of scalar values (string | number | boolean), at most 25 keys.logScreen(name) โ void โ convenience for a screen_view event.trace(name, ms) โ void โ record a custom performance measurement, in milliseconds.recordError(err) โ void โ report a caught error. err can be an Error, a { message?, stack? } object, or a string. Flushes immediately.setUserId(id) โ void โ tie analytics to a real app user id (opt-in). Pass null or '' to clear it (e.g. on sign-out).setUserProperties(props) โ void โ set segmentation properties; they're merged, persisted, and attached to events.flush() โ Promise<Result> โ force-send buffered events now.The workhorse is logEvent. Give it a name, and optionally a flat bag of scalar params for context:
lingcode.telemetry.logEvent('checkout_started', { plan: 'pro', amount: 29 });
Params are for slicing later โ plan tier, item count, the variant a user saw. Keep them scalar (string, number, or boolean) and keep the count at or under 25 keys; nested objects and overflowing keys don't belong in an event.
Screen views get their own shorthand so you don't have to remember an event-name convention:
lingcode.telemetry.logScreen('Pricing');
That's exactly equivalent to a screen_view event for the Pricing screen โ call it from each route or view as the user navigates.
By default events aren't attached to a person. Once someone signs in, opt in by handing the telemetry layer their app user id; clear it again when they sign out so the next session starts clean:
// after a successful sign-in
lingcode.telemetry.setUserId(user.id);
// on sign-out
lingcode.telemetry.setUserId(null);
For segmentation โ plan tier, locale, cohort โ set user properties. They're merged with whatever you set before, persisted on the client, and attached to subsequent events:
lingcode.telemetry.setUserProperties({ plan: 'pro', locale: 'en-US', cohort: '2026-Q2' });
Reach for recordError in your global error handler and in any catch block worth knowing about. It accepts an Error, a plain { message?, stack? } object, or a bare string:
// a global handler
window.addEventListener('error', (e) => {
lingcode.telemetry.recordError(e.error ?? e.message);
});
// or in a catch block
try {
await doRiskyThing();
} catch (err) {
lingcode.telemetry.recordError(err);
}
Unlike the other calls, recordError flushes immediately โ a crash that takes the page down with it shouldn't take its own report along too.
Use trace to record a custom performance measurement in milliseconds โ how long an image took to load, how long a query ran, how long a screen took to become interactive:
const start = performance.now();
await loadHeroImage();
lingcode.telemetry.trace('image_load', performance.now() - start);
// or pass a measurement you already have
lingcode.telemetry.trace('image_load', 320);
Events batch and flush automatically โ you don't normally call flush() yourself. The two cases where it earns its keep: before a hard navigation (a full-page redirect or close) where you need delivery guaranteed, and any moment you want buffered events sent right now. recordError already flushes on its own, so crash reports aren't lost. Keep event params scalar and within 25 keys, and lean on setUserProperties for the segmentation you'll filter by โ plan tier, locale, cohort.
flush() returns a promise so you can await delivery before the page goes away:
// guarantee buffered events are sent before a hard navigation
await lingcode.telemetry.flush();
window.location.href = '/checkout';
The SDK posts batched events to your backend's telemetry endpoint. You rarely call this directly, but it's here for non-JS clients and debugging:
POST https://lingcode.dev/api/cloud/be/<backend-id>/telemetry
Content-Type: application/json
{
"events": [
{ "name": "checkout_started", "params": { "plan": "pro", "amount": 29 }, "timestamp": 1718064000000 }
]
}
A failed send surfaces as telemetry_failed.
setUserId. Only attach a real user id once you have a basis to โ a signed-in session and whatever consent your app requires โ and clear it with setUserId(null) on sign-out so a shared device doesn't blend two people's analytics.