Devtoberfest cat-game (Kasimir) & leaderboard β PROD data troubleshooting runbook β
Audience: operators/engineers debugging the "Tag the Cat"/Kasimir counter, the Devtoberfest leaderboard, duplicate-user/identity-fork issues, or stranded points (e.g. issue #2651).
Golden rule: the live cat-game data is NOT in the container that a plain cds bind resolves to. Read "Where the data actually lives" first or you will waste hours querying an empty/stale container (this happened on 2026-10-05).
Where the data actually lives β
The tutorials-hana HANA instance hosts multiple HDI container schemas:
| Schema | What it is | Cat-game data? |
|---|---|---|
2FEC6DAB47984DB482E0B3BB80AFFEF7 | The container bound by tutorials-srv and gameboard-srv at runtime | β YES β this is the real one (~817k Users, ~3.2k CatGameAwards) |
AC9753D6β¦ | What cds bind --to tutorials-hana:tutorials-hana-key lands in | β decoy β ~2 award rows only |
The prod HANA host is 8996974c-β¦.hana.prod-eu10.hanacloud.ondemand.com (the prod-eu10 in the hostname is your confirmation you're on PROD, not DEV).
gameboard-db (own instance) is effectively empty (CDS_OUTBOX only). devtoberfest-planner-db is the conference session planner, not cat-game. Both are red herrings.
Why cds bind lands in the wrong place β
cds bind --to tutorials-hana:tutorials-hana-key uses the service key tutorials-hana-key, which maps to container AC9753D6β¦. The running apps bind the same instance via a different HDI container (2FEC6DABβ¦) delivered through their service binding, not that key. Trust the app's live binding, not the service key.
How to connect read-only to the correct container β
Use the live binding of a running app that already points at 2FEC6DAB⦠(tutorials-srv or gameboard-srv):
cf target # MUST show org tutorial-system / space prod
cf env tutorials-srv > /tmp/srv-env.txt # or: cf env gameboard-srvParse VCAP_SERVICES β hana[] β the instance named tutorials-hana β .credentials (host, port, user, password, certificate, schema = 2FEC6DABβ¦).
Connect with the hdb node client (installed in this repo):
const hdb = require('hdb');
const client = hdb.createClient({
host: cc.host, port: cc.port, user: cc.user, password: cc.password,
ca: cc.certificate, encrypt: true, sslValidateCertificate: !!cc.certificate,
});β οΈ hdb client gotchas (both cost time on 2026-10-05) β
- Positional
?bound params FAIL in this environment (not all variables bound). Inline escaped literals instead β`WHERE email = '${s.replace(/'/g,"''")}'`. (@sap/hana-clientwould support params, buthdbis what's installed.) - The service-key login lands in a
β¦_RTruntime schema; the tables are in the container schema2FEC6DABβ¦. Always fully-qualify:"2FEC6DAB47984DB482E0B3BB80AFFEF7"."TABLE_NAME". QueryingCURRENT_SCHEMAfinds nothing.
Key objects (schema 2FEC6DABβ¦) β
Tables (COM_SAP_DEVELOPERS_IMS_*):
| Table | Holds | Rows (2026-10-05) |
|---|---|---|
_USERS | user accounts | ~817,424 (only ~20,671 have an email) |
_CATGAMEAWARDS | per-day Kasimir catches (user_ID, event_ID, awardDate, points; 5/day, 100/event caps) | ~3,244 |
_EVENTREGISTRATIONS | event opt-in (user_ID, event_ID, joinedAt) | ~2,692 |
_USERIDENTITIES | login links (issuer, subject) | ~1,826 |
_EVENTS | events; eventType='DEVTOBERFEST', startDate/endDate | β |
Leaderboard views (defined in db/src/*.hdbview):
GAMEBOARD_PARTICIPANT_V1βFROM EVENTREGISTRATIONS r JOIN USERS JOIN EVENTS WHERE eventType='DEVTOBERFEST'. Driven by registrations. A user with awards but no registration row is invisible here β this is the core stranding mechanism.GAMEBOARD_BONUS_V1β cat-game bonus points surfaced to the leaderboard.GAMEBOARD_COMPLETION_V1β completions scored by event date window[startDate,endDate](retroactive, multi-event), NOT byTaskRecords.event_ID.GAMEBOARD_ACTIVE_EVENT_V1β current event.
The #2651 class of bug (identity fork β stranded points) β
Symptom: in-game Kasimir counter resets/climbs from a low base while the Devtoberfest leaderboard total is frozen; or vice-versa.
Mechanism: the same human has β₯2 Users rows (same email, different sapId). New catches land on the newer fork row; the counter sums that row and looks "reset". The fork row has no EventRegistrations, so GAMEBOARD_PARTICIPANT_V1's registration-driven join hides its points from the leaderboard, which still shows the older row's registered total.
Prevention fix (shipped, PR #2661 / v1.36.0): tokenEmail() now reads IAS tokens' user.attr.email (not just payload.email), so returning SAP-ID/IAS logins resolve to the existing row instead of minting a fork. Plus pickCanonicalRow + provisionDbUser hardening.
Reconciliation of existing forks: reconcileDuplicateEmails (srv/jobs/identity-dedup-job.js) β mergeAccounts (srv/lib/account-merge.js). Deliberately NOT in JOB_REGISTRY (operator-invoked only). See identity-dedup-reconciliation.md. It must run against 2FEC6DABβ¦ β running it via cds bind β¦ tutorials-hana-key hits the decoy container and reports a false near-empty no-op.
The merge is a soft-merge: it repoints TaskRecords/PrizeRecords/AccomplishmentRecords/UserIdentities/EventRegistrations/CatGameAwards from secondaryβprimary, caps CatGameAwards (5/day, 100/event), and writes a SecondaryAccounts audit row (status MERGED, mergedAt, primaryAccount_ID). It does NOT delete the secondary Users row, so the email cluster still exists afterward. The job has no idempotency guard β do not run it twice; a second pass re-merges now-empty secondaries (harmless to points, but writes duplicate audit rows).
Ready-to-use read-only queries β
Replace S with 2FEC6DAB47984DB482E0B3BB80AFFEF7 and inline literals (no bound params).
-- A user's forked rows + their award/registration split (the #2651 picture)
SELECT ID, email, sapId, createdAt FROM "S"."COM_SAP_DEVELOPERS_IMS_USERS"
WHERE LOWER(email) = LOWER('<email>');
-- per row:
SELECT COUNT(*) n, COALESCE(SUM(points),0) pts
FROM "S"."COM_SAP_DEVELOPERS_IMS_CATGAMEAWARDS" WHERE user_ID = '<row-ID>';
SELECT COUNT(*) n
FROM "S"."COM_SAP_DEVELOPERS_IMS_EVENTREGISTRATIONS" WHERE user_ID = '<row-ID>';
-- Whole-container dup-email clusters
SELECT COUNT(*) FROM (SELECT LOWER(email) e
FROM "S"."COM_SAP_DEVELOPERS_IMS_USERS" WHERE email IS NOT NULL
GROUP BY LOWER(email) HAVING COUNT(*) > 1);
-- Stranded cat-game points: awards on rows with no participant/registration
SELECT COUNT(DISTINCT a.user_ID) users, COUNT(*) rows_, COALESCE(SUM(a.points),0) pts
FROM "S"."COM_SAP_DEVELOPERS_IMS_CATGAMEAWARDS" a
WHERE NOT EXISTS (SELECT 1 FROM "S"."COM_SAP_DEVELOPERS_IMS_EVENTREGISTRATIONS" r
WHERE r.user_ID = a.user_ID);
-- A user's leaderboard visibility (participant row only exists if registered)
SELECT COUNT(*) FROM "S"."GAMEBOARD_PARTICIPANT_V1" WHERE "USER_ID" = '<row-ID>';Measured 2026-10-05 (PROD, container 2FEC6DABβ¦): 374 dup-email clusters; 142 users / 172 award rows / 860 points stranded. Reporter daniela.fojtzik@all-for-one.com: canonical 25bae84a-β¦ (55 pts, 1 reg) + fork b5649b00-β¦ (25 pts, 0 reg) β after merge, canonical carried 75 pts (5 trimmed to the per-day/event cap) and remained leaderboard-visible.
Running the reconcile job against the correct container (operator) β
Because cds bind resolves to the decoy container, feed CAP the live tutorials-srv binding so the real job code resolves cds.entities against 2FEC6DABβ¦:
cf env tutorials-srvβ extractVCAP_SERVICES.hanaβ write adefault-env.jsonof{ "VCAP_SERVICES": { "hana": [...] } }to a scratch dir outside the repo (never commit creds).- In a node runner: set
process.env.VCAP_SERVICESfrom that file +CDS_REQUIRES_DB_KIND=hana,chdirto the repo,cds.connect.to('db'). - Assert
CURRENT_SCHEMAstarts with2FEC6DABand abort otherwise β the mandatory guard against the wrong-container trap. import('./srv/jobs/identity-dedup-job.js')βreconcileDuplicateEmails({ dryRun: true }), review, then{dryRun:false, limit:1}canary, then full. See identity-dedup-reconciliation.md for the gate/verify steps.
Note on
cf envoutput: it prints human-readable text before the JSON β don't pipe straight tojq. Extract theVCAP_SERVICESobject by brace-matching first (a small Python/node snippet), then parse.