Skip to content

react native support#997

Open
goldflag wants to merge 1 commit into
masterfrom
react-native-support
Open

react native support#997
goldflag wants to merge 1 commit into
masterfrom
react-native-support

Conversation

@goldflag
Copy link
Copy Markdown
Collaborator

@goldflag goldflag commented May 17, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for mobile app analytics via React Native SDK
    • Mobile apps now display "App Analytics" alongside "Web Analytics"
    • New site creation flow supports both websites and mobile apps with app identifier input
    • React Native SDK configuration with initial screen view, outbound link tracking, and error event options
  • Documentation

    • Added React Native SDK installation and integration guides
  • Chores

    • Updated localization files across all languages
    • Database schema expanded to support mobile app type designation

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
rybbit Ready Ready Preview, Comment May 17, 2026 5:20am

Request Review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 17, 2026

📝 Walkthrough

Walkthrough

This PR introduces mobile site type support to the analytics platform alongside a complete React Native SDK. It adds a type field to sites (web or mobile), updates the database schema and all related APIs, implements conditional UI rendering based on site type, and enhances the tracking pipeline with client-side anonymous ID generation.

Changes

Mobile site type and React Native SDK support

Layer / File(s) Summary
Database schema and core type definitions
server/drizzle/0006_site_type.sql, server/drizzle/meta/0006_snapshot.json, server/drizzle/meta/_journal.json, server/src/db/postgres/schema.ts, server/src/lib/siteConfig.ts
Migration adds nullable type column with check constraint limiting values to web, mobile, or NULL. Drizzle schema and schema snapshot are updated. SiteConfigData interface gains type: "web" | "mobile" field.
API type contracts for sites and admin
client/src/api/admin/endpoints/adminSites.ts, client/src/api/admin/endpoints/sites.ts
AdminSiteData, SiteResponse, and GetSitesFromOrgResponse add nullable type field. addSite and updateSiteConfig parameters now accept optional type field in request bodies.
Server site management endpoints with type support
server/src/api/sites/addSite.ts, server/src/api/sites/updateSiteConfig.ts, server/src/api/sites/getSite.ts, server/src/api/sites/getSitesFromOrg.ts, server/src/api/admin/getAdminSites.ts, server/src/api/sites/getTrackingConfig.ts
Endpoints now validate site type; web sites require domain regex match, mobile sites require app identifier regex match. sessionReplay and webVitals are forced to false for mobile sites. Type defaults to "web" in responses when unset; domain defaults to empty string.
React Native SDK package implementation
react-native/package.json, react-native/index.d.ts, react-native/index.js, react-native/README.md
New @rybbit/react-native package exports RybbitReactNative class and default instance. Implements SDK initialization, event/screen/page/error/identify tracking, trait management, React Navigation integration, and app lifecycle management. Full TypeScript declarations and comprehensive README provided.
Client site creation dialog with type selection
client/src/app/components/AddSite.tsx, client/src/app/[site]/components/Sidebar/SiteSelector.tsx
AddSite dialog now shows radio group to choose between website and React Native app. Form validation and normalization are conditional: domains use domain regex and normalizeDomain for web, app identifiers trimmed and validated with identifier regex for mobile. Dialog resets to web site type on close.
Site Settings UI for mobile and web sites
client/src/components/SiteSettings/GeneralTab.tsx, client/src/components/SiteSettings/ScriptBuilder.tsx, client/src/components/SiteSettings/SiteSettings.tsx, client/src/components/SiteSettings/TrackingTab.tsx, client/src/app/[site]/components/Sidebar/Sidebar.tsx
UI components detect mobile sites via type === "mobile" and conditionally render features. Identifier field label switches between "App Identifier" and "Domain"; ScriptBuilder renders React Native SDK setup or web tracking script; tab label changes to "React Native SDK"; sidebar hides web-only features (Performance, Replay) and changes "Web Analytics" to "App Analytics"; TrackingTab hides web-only toggles (Session Replay, Web Vitals, Outbound Links, Form tracking) and adjusts labels for mobile.
Server tracking pipeline with anonymous ID support
server/src/services/userId/userIdService.ts, server/src/services/tracker/identifyService.ts, server/src/services/tracker/trackEvent.ts, server/src/services/tracker/utils.ts
trackEvent and identify payloads now accept optional anonymous_id field. When present, a deterministic user ID is generated from client ID and site ID with optional daily salt via new generateUserIdFromClientId() method; otherwise falls back to IP/user-agent based derivation. Anonymous ID derivation and user alias creation logic updated accordingly.
Documentation and internationalization
docs/content/docs/sdks/meta.json, docs/content/docs/sdks/react-native.mdx, client/messages/cs.json, client/messages/de.json, client/messages/en.json, client/messages/es.json, client/messages/fr.json, client/messages/it.json, client/messages/ja.json, client/messages/ko.json, client/messages/pl.json, client/messages/pt.json, client/messages/uk.json, client/messages/zh.json
Documentation metadata updated to include React Native SDK. React Native SDK documentation page added with installation, initialization, usage examples, React Navigation integration guide, and notes on web-only features. All 11 language files updated with new translation keys for "App Analytics", "Add Site", React Native SDK setup messages, and app identifier validation/success messages. Two web-only keys removed and repositioned keys maintained without value changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • rybbit-io/rybbit#879: Both PRs enhance the user-identification tracking pipeline by updating identifyService.ts and utils.ts to handle optional anonymous_id and modify how user IDs are derived and persisted.

  • rybbit-io/rybbit#837: Both PRs modify ScriptBuilder.tsx to render different script configurations; this PR adds mobile/web conditional logic while the related PR adds verification installation flow.

  • rybbit-io/rybbit#602: This PR adds trackIp field support to site configuration APIs (updateSiteConfig payload) that the related PR introduces end-to-end.

Poem

🐰 A mobile site hops into view,
With React Native apps now tracked too,
Web and mobile, side by side,
Anonymous IDs to guide the tide,
New SDK ready to compile!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'react native support' directly describes the primary change: adding React Native SDK support throughout the codebase including new package, documentation, UI conditionals, and database migrations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch react-native-support

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
client/src/components/SiteSettings/GeneralTab.tsx (1)

121-137: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate mobile identifiers before submit and return mobile-specific failure text.

In Line 129, mobile updates are only trimmed and sent; malformed values are deferred to server rejection, and Line 136 still reports a domain-specific failure for mobile sites.

Suggested fix
   const handleDomainChange = async () => {
-    if (!newDomain) {
+    const trimmedValue = newDomain.trim();
+    if (!trimmedValue) {
       toast.error(isMobileSite ? t("App identifier cannot be empty") : t("Domain cannot be empty"));
       return;
     }
+    if (isMobileSite && !/^(?!.*\.\.)(?!\.)(?!.*\.$)[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*)+$/.test(trimmedValue)) {
+      toast.error(t("Invalid app identifier. Use a bundle/package identifier like com.example.app"));
+      return;
+    }

     try {
       setIsChangingDomain(true);
-      const normalizedDomain = isMobileSite ? newDomain.trim() : normalizeDomain(newDomain);
+      const normalizedDomain = isMobileSite ? trimmedValue : normalizeDomain(trimmedValue);
       await updateSiteConfig(siteMetadata.siteId, { domain: normalizedDomain });
       toast.success(isMobileSite ? t("App identifier updated successfully") : t("Domain updated successfully"));
       router.refresh();
       refetch();
     } catch (error) {
       console.error("Error changing domain:", error);
-      toast.error(t("Failed to update domain"));
+      toast.error(isMobileSite ? t("Failed to update app identifier") : t("Failed to update domain"));
     } finally {
       setIsChangingDomain(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/components/SiteSettings/GeneralTab.tsx` around lines 121 - 137,
The handleDomainChange currently only trims mobile identifiers and still uses
domain-specific error messages; update it to perform client-side validation for
mobile sites: after computing normalizedDomain (from newDomain.trim() when
isMobileSite), validate against the mobile identifier rules (non-empty, allowed
characters/format) and if invalid call toast.error with the mobile-specific
message (e.g., t("Invalid app identifier")) and return; also in the catch block
use the mobile-specific failure text when isMobileSite (instead of the domain
message) so updateSiteConfig failures show the correct mobile error; refer to
handleDomainChange, isMobileSite, newDomain, normalizeDomain, updateSiteConfig,
and the toast.* calls to locate where to add the validation and conditional
error messaging.
server/src/services/tracker/identifyService.ts (1)

101-127: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Collapse the alias write into a single upsert.

This is still a select-then-write race. Two concurrent identify calls for the same anonymousId can both miss the row; one insert wins and the other falls into the catch block, leaving the alias on the wrong user_id and skipping the backfill path. A single onConflictDoUpdate(...) write avoids that nondeterminism.

Suggested direction
-        // Check if alias already exists
-        const existingAlias = await db
-          .select()
-          .from(userAliases)
-          .where(and(eq(userAliases.siteId, siteId), eq(userAliases.anonymousId, anonymousId)))
-          .limit(1);
-
-        if (existingAlias.length === 0) {
-          // Create new alias
-          await db.insert(userAliases).values({
-            siteId,
-            anonymousId,
-            userId: user_id,
-          });
-          // Fire-and-forget: backfill identified_user_id on past anonymous events
-          backfillIdentifiedUserId(siteId, anonymousId, user_id);
-        } else if (existingAlias[0].userId !== user_id) {
-          // Update alias to point to new user
-          await db
-            .update(userAliases)
-            .set({ userId: user_id })
-            .where(and(eq(userAliases.siteId, siteId), eq(userAliases.anonymousId, anonymousId)));
-        }
+        await db
+          .insert(userAliases)
+          .values({
+            siteId,
+            anonymousId,
+            userId: user_id,
+          })
+          .onConflictDoUpdate({
+            target: [userAliases.siteId, userAliases.anonymousId],
+            set: { userId: user_id },
+          });
+
+        // Trigger backfill according to the intended insert/update semantics.
+        backfillIdentifiedUserId(siteId, anonymousId, user_id);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/services/tracker/identifyService.ts` around lines 101 - 127, The
current select-then-insert/update in the identifyService.ts around userAliases
is racy: replace the read-then-write with a single upsert using
db.insert(...).onConflictDoUpdate(...) (targeting userAliases.siteId +
userAliases.anonymousId) so concurrent identify calls deterministically set
userId; ensure the upsert's update clause sets userId to the incoming user_id
and returns whether the row was inserted vs updated (or check returned rows) so
you can call backfillIdentifiedUserId(siteId, anonymousId, user_id) only when
the alias was newly created (or when userId actually changed), removing the
select and catching unique-constraint races in the try/catch around this block.
🧹 Nitpick comments (1)
react-native/index.d.ts (1)

59-72: 💤 Low value

Consider making getUserId() async for API consistency.

The getUserId() method returns string | null synchronously, while all other SDK methods are async and return Promise<void> or Promise<T>. This inconsistency may confuse developers expecting a uniform async API throughout the SDK.

If the user ID is always available in memory (no async storage read needed), document this explicitly. Otherwise, consider making it async: getUserId(): Promise<string | null> to match the rest of the API surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react-native/index.d.ts` around lines 59 - 72, The getUserId method on class
RybbitReactNative is synchronous while the rest of the SDK surface is async;
either document that userId is always held in memory or change the signature to
async for consistency—update the declaration in RybbitReactNative from
getUserId(): string | null to getUserId(): Promise<string | null>, update any
implementations of getUserId to return a Promise (resolve the in-memory value or
await storage), and adjust any callers to await RybbitReactNative.getUserId()
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@client/messages/cs.json`:
- Line 68: The Czech translations file client/messages/cs.json contains multiple
keys with empty values (e.g., "erYzJH", "FIKvUB", "KI57sV", "AZEGID", "JkLHGw",
"CXI0EW", "VQxsRK", "W35dz6", "2YpSsF", "HtTwhf", "zoRFJV", "pm+3nT", "0Q1jqR",
"8s+sTd", "495/Ut", "lxHUeO") that must be completed; open cs.json, locate each
empty key (also present at the other mentioned occurrences) and replace the ""
with accurate Czech translations matching the feature/context (mobile site
support, app identifier validation, tracking UI labels), ensuring string
formatting and placeholders match the English source and run a quick
linter/locale check after updating.

In `@client/messages/de.json`:
- Line 68: Several German translation keys are empty which will render blank UI
text; locate the entries for keys erYzJH, FIKvUB, KI57sV, AZEGID, JkLHGw,
CXI0EW, VQxsRK, W35dz6, 2YpSsF, HtTwhf, zoRFJV, pm+3nT, 0Q1jqR, 8s+sTd, 495/Ut
and lxHUeO and replace the empty string values with appropriate German
translations (matching the intent of their English originals for mobile site
features, app ID validation, and tracking text); ensure formatting/escaping
matches the surrounding JSON, run the i18n/lint or build checks to validate
there are no missing keys or formatting errors, and include meaningful,
context-aware German phrases for each key.

In `@client/messages/es.json`:
- Line 68: Several i18n keys (e.g., "erYzJH" and the other keys listed at lines
87, 636, 640-642, 644, 650, 1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) are
set to empty strings which yields blank UI text; for each such key either
provide the proper Spanish translation string or remove the key entirely from
client/messages/es.json until a translation is available so the i18n runtime can
fall back to the default locale—update the values for keys like "erYzJH" (and
the others referenced) with their Spanish text or delete those entries to avoid
blank labels.

In `@client/messages/fr.json`:
- Line 68: The JSON contains empty-string translations (e.g., key "erYzJH" and
the other listed keys at lines mentioned) which must not be shipped; either
provide proper French translations for each listed key (replace "" with the
translated string) or remove/omit the keys temporarily from
client/messages/fr.json until translations are available, then run the i18n
build/validation step to ensure no empty values remain; specifically update the
keys "erYzJH" and the other keys called out (lines 87, 636, 640-642, 644, 650,
1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) so none have empty values.

In `@client/messages/it.json`:
- Line 68: Several Italian translation entries are empty (e.g., the key "erYzJH"
and also keys at positions corresponding to the other empty entries listed);
either provide proper Italian copy for each empty JSON key or remove the key
entirely so the app falls back to the default locale. Update the JSON keys
"erYzJH" and the other empty keys (the ones reported at 87, 636, 640-642, 644,
650, 1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) with valid Italian strings
or delete those key entries to avoid rendering blank UI text.

In `@client/messages/ja.json`:
- Line 68: The Japanese messages file contains empty translation values (e.g.,
the key "erYzJH" and the other listed keys), which will render blank labels;
replace each empty string with the proper Japanese translation or temporarily
remove the key until a localized copy is available, ensuring you update the same
keys (e.g., "erYzJH") in client/messages/ja.json and keep JSON syntax valid;
after edits run the i18n/localization linter or build to confirm no missing
keys/formatting issues.

In `@client/messages/ko.json`:
- Line 68: Replace empty translation values in the Korean locale with at least
temporary fallback copy (e.g., the original English source text) so UI labels
don't render blank; specifically update the "erYzJH" entry to a non-empty string
and find and fix the other empty keys referenced in the comment (the entries at
the indicated positions such as those around lines 87, 636, 640-642, 644, 650,
1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) in client/messages/ko.json by
populating each empty value with the English source or plausible placeholder
until proper Korean translations are available.

In `@client/messages/pl.json`:
- Line 68: Several newly added localization keys (e.g., "erYzJH" plus the other
keys called out at lines like 87, 636, 640-642, 644, 650, 1105, 1107, 1128,
1183-1184, 1223, 1225, 1233) are empty strings in client/messages/pl.json;
replace each empty value with an appropriate Polish translation or a temporary
Polish fallback copy (e.g., English fallback wrapped in Polish context) so the
UI won't render blank labels/messages—update the entries for keys "erYzJH" and
the other listed keys to non-empty Polish text while preserving the exact JSON
keys and formatting.

In `@client/messages/pt.json`:
- Line 68: The JSON translation entry "erYzJH" (and the other empty keys listed:
entries at 87, 636, 640-642, 644, 650, 1105, 1107, 1128, 1183-1184, 1223, 1225,
1233) are empty placeholders and must be populated; open
client/messages/pt.json, locate each empty key (e.g., "erYzJH") and provide the
correct Portuguese translation for the source string, or if unsure copy the
original English source text as a temporary fallback so the UI won't show blank
text. Ensure each replaced value is a valid JSON string (escaped as needed) and
keep the key names unchanged.

In `@client/messages/uk.json`:
- Line 68: Several message keys in the Ukrainian localization are empty and must
be populated; update the entries for keys "erYzJH" and the other affected keys
(those at positions you noted: the ones corresponding to lines 87, 636, 640-642,
644, 650, 1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) so they are not empty
strings—either provide proper Ukrainian translations or insert a temporary
fallback (e.g., the English source text) for each key; ensure you edit the
uk.json entries for each listed key (leave JSON formatting intact) and run a
quick lint/validate of the JSON to confirm no syntax errors.

In `@client/messages/zh.json`:
- Line 68: Several Chinese locale keys were left as empty strings (e.g.,
"erYzJH" and the other keys at the comment: those at positions 87, 636, 640-642,
644, 650, 1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) causing blank labels;
update each of these keys in the zh.json file to provide either the proper
Chinese translation or a temporary fallback English copy so the app doesn't
render empty text, ensuring you edit the exact JSON keys (like "erYzJH") and
keep valid JSON string values.

In `@client/src/app/components/AddSite.tsx`:
- Around line 32-33: The isValidAppIdentifier validator currently allows
single-segment names like "myapp" and permits a leading digit; update the
function (isValidAppIdentifier) to enforce a package/bundle style (dot-separated
segments, at least two segments) where each segment starts with a letter and
uses only letters, digits, underscores, hyphens or dots as separators, and
preserve overall length limits (e.g. max 253 chars total). Replace the current
regex with one that requires ^[A-Za-z][A-Za-z0-9_-]*(\.[A-Za-z][A-Za-z0-9_-]*)+$
so identifiers like "com.example.app" are valid but "myapp" or "1app" are
rejected; update any tests or usage that rely on the old behavior.

In `@react-native/index.js`:
- Line 3: The SDK_VERSION constant in react-native/index.js is hardcoded and can
drift from package.json; replace the literal SDK_VERSION with the package
version by importing the version field from package.json (e.g., require or
import the JSON and use its .version) and assign that to SDK_VERSION so it
always matches package.json; update any references to SDK_VERSION to use the new
imported value.
- Around line 204-227: createBasePayload currently falls back to
this.config.siteId for the hostname which can put numeric site IDs into the
hostname field; enforce a proper app identifier by (1) changing init() to
validate that this.config.appIdentifier (or context.appIdentifier) is present
for mobile/React Native usage and throw or log a clear error if missing, and (2)
updating createBasePayload to use only context.appIdentifier ||
this.config.appIdentifier for hostname (do not fallback to this.config.siteId);
reference createBasePayload and init (or the module initialization path) and
update any docs/tests that construct the client so mobile integrations supply a
bundle/app identifier like com.example.app.

In `@server/src/api/sites/addSite.ts`:
- Around line 70-80: The current appIdentifierRegex is too permissive and allows
single-token or trailing-dot values; update the regex used in the siteType ===
"mobile" validation (symbol: appIdentifierRegex and variable cleanedDomain in
addSite.ts) to enforce bundle/package format: require at least two dot-separated
components, no leading/trailing/consecutive dots, each component starting with a
letter and containing only letters, digits or underscores/hyphens as allowed
(e.g. replace with something like
^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$), then use that regex in
the existing if (siteType === "mobile" &&
!appIdentifierRegex.test(cleanedDomain)) branch and update the error message to
mention the required dot-separated bundle identifier format.

In `@server/src/api/sites/updateSiteConfig.ts`:
- Around line 87-88: The code uses nextDomain = updateData.domain ?? site.domain
and immediately calls replace() on it, which can throw when site.domain is
nullish; update the logic around nextDomain/cleanedDomain in updateSiteConfig.ts
to guard for null/undefined before calling replace(): compute nextDomain as
before, then if nextDomain is nullish skip normalization (e.g., leave
cleanedDomain undefined or null) or normalize via a safe fallback like
(nextDomain ?? "") before replace, and ensure the later domain/type validation
still rejects absent domains as intended; update references to nextDomain and
cleanedDomain accordingly so unrelated PATCHes don't hit replace() on nullish
site.domain.

In `@server/src/services/userId/userIdService.ts`:
- Around line 70-74: The generateUserIdFromClientId function is using
getDailySalt(), causing client-derived anonymous_id hashes to change each UTC
day; replace the daily-rotating salt with a stable site-specific salt so the
same clientId always hashes to the same user id across days. Update
generateUserIdFromClientId to use a persistent salt from the site config (e.g.,
a config.userIdSalt or config.salt) or an empty string when saltUserIds is false
instead of calling this.getDailySalt(), ensuring the hash input remains stable;
adjust any references to getDailySalt() here to a stable-salt accessor or field.

---

Outside diff comments:
In `@client/src/components/SiteSettings/GeneralTab.tsx`:
- Around line 121-137: The handleDomainChange currently only trims mobile
identifiers and still uses domain-specific error messages; update it to perform
client-side validation for mobile sites: after computing normalizedDomain (from
newDomain.trim() when isMobileSite), validate against the mobile identifier
rules (non-empty, allowed characters/format) and if invalid call toast.error
with the mobile-specific message (e.g., t("Invalid app identifier")) and return;
also in the catch block use the mobile-specific failure text when isMobileSite
(instead of the domain message) so updateSiteConfig failures show the correct
mobile error; refer to handleDomainChange, isMobileSite, newDomain,
normalizeDomain, updateSiteConfig, and the toast.* calls to locate where to add
the validation and conditional error messaging.

In `@server/src/services/tracker/identifyService.ts`:
- Around line 101-127: The current select-then-insert/update in the
identifyService.ts around userAliases is racy: replace the read-then-write with
a single upsert using db.insert(...).onConflictDoUpdate(...) (targeting
userAliases.siteId + userAliases.anonymousId) so concurrent identify calls
deterministically set userId; ensure the upsert's update clause sets userId to
the incoming user_id and returns whether the row was inserted vs updated (or
check returned rows) so you can call backfillIdentifiedUserId(siteId,
anonymousId, user_id) only when the alias was newly created (or when userId
actually changed), removing the select and catching unique-constraint races in
the try/catch around this block.

---

Nitpick comments:
In `@react-native/index.d.ts`:
- Around line 59-72: The getUserId method on class RybbitReactNative is
synchronous while the rest of the SDK surface is async; either document that
userId is always held in memory or change the signature to async for
consistency—update the declaration in RybbitReactNative from getUserId(): string
| null to getUserId(): Promise<string | null>, update any implementations of
getUserId to return a Promise (resolve the in-memory value or await storage),
and adjust any callers to await RybbitReactNative.getUserId() accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54ead776-c6e5-417b-8000-052113750926

📥 Commits

Reviewing files that changed from the base of the PR and between d527c38 and ce76749.

📒 Files selected for processing (42)
  • client/messages/cs.json
  • client/messages/de.json
  • client/messages/en.json
  • client/messages/es.json
  • client/messages/fr.json
  • client/messages/it.json
  • client/messages/ja.json
  • client/messages/ko.json
  • client/messages/pl.json
  • client/messages/pt.json
  • client/messages/uk.json
  • client/messages/zh.json
  • client/src/api/admin/endpoints/adminSites.ts
  • client/src/api/admin/endpoints/sites.ts
  • client/src/app/[site]/components/Sidebar/Sidebar.tsx
  • client/src/app/[site]/components/Sidebar/SiteSelector.tsx
  • client/src/app/components/AddSite.tsx
  • client/src/components/SiteSettings/GeneralTab.tsx
  • client/src/components/SiteSettings/ScriptBuilder.tsx
  • client/src/components/SiteSettings/SiteSettings.tsx
  • client/src/components/SiteSettings/TrackingTab.tsx
  • docs/content/docs/sdks/meta.json
  • docs/content/docs/sdks/react-native.mdx
  • react-native/README.md
  • react-native/index.d.ts
  • react-native/index.js
  • react-native/package.json
  • server/drizzle/0006_site_type.sql
  • server/drizzle/meta/0006_snapshot.json
  • server/drizzle/meta/_journal.json
  • server/src/api/admin/getAdminSites.ts
  • server/src/api/sites/addSite.ts
  • server/src/api/sites/getSite.ts
  • server/src/api/sites/getSitesFromOrg.ts
  • server/src/api/sites/getTrackingConfig.ts
  • server/src/api/sites/updateSiteConfig.ts
  • server/src/db/postgres/schema.ts
  • server/src/lib/siteConfig.ts
  • server/src/services/tracker/identifyService.ts
  • server/src/services/tracker/trackEvent.ts
  • server/src/services/tracker/utils.ts
  • server/src/services/userId/userIdService.ts

Comment thread client/messages/cs.json
"tL4WC1": "Načítám další...",
"HPx76n": "Všechny položky načteny",
"QkTyho": "Postranní panel Rybbit",
"erYzJH": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Complete Czech translations for mobile site features.

Multiple translation keys have been added with empty string values (""), which will display as blank text in the Czech UI. These appear to be for mobile site type support, app identifier validation, and tracking features.

Empty translation keys: erYzJH, FIKvUB, KI57sV, AZEGID, JkLHGw, CXI0EW, VQxsRK, W35dz6, 2YpSsF, HtTwhf, zoRFJV, pm+3nT, 0Q1jqR, 8s+sTd, 495/Ut, lxHUeO.

Consider completing these translations before the first stable release to ensure a complete user experience for Czech-speaking users.

Also applies to: 87-87, 636-636, 640-642, 644-644, 650-650, 1105-1105, 1107-1107, 1128-1128, 1183-1184, 1223-1223, 1225-1225, 1233-1233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/cs.json` at line 68, The Czech translations file
client/messages/cs.json contains multiple keys with empty values (e.g.,
"erYzJH", "FIKvUB", "KI57sV", "AZEGID", "JkLHGw", "CXI0EW", "VQxsRK", "W35dz6",
"2YpSsF", "HtTwhf", "zoRFJV", "pm+3nT", "0Q1jqR", "8s+sTd", "495/Ut", "lxHUeO")
that must be completed; open cs.json, locate each empty key (also present at the
other mentioned occurrences) and replace the "" with accurate Czech translations
matching the feature/context (mobile site support, app identifier validation,
tracking UI labels), ensuring string formatting and placeholders match the
English source and run a quick linter/locale check after updating.

Comment thread client/messages/de.json
"tL4WC1": "Lade mehr...",
"HPx76n": "Alle Einträge geladen",
"QkTyho": "Rybbit-Seitenleiste",
"erYzJH": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Complete German translations for mobile site features.

Multiple translation keys have been added with empty string values (""), which will display as blank text in the German UI. These appear to be for mobile site type support, app identifier validation, and tracking features.

Empty translation keys: erYzJH, FIKvUB, KI57sV, AZEGID, JkLHGw, CXI0EW, VQxsRK, W35dz6, 2YpSsF, HtTwhf, zoRFJV, pm+3nT, 0Q1jqR, 8s+sTd, 495/Ut, lxHUeO.

Consider completing these translations before the first stable release to ensure a complete user experience for German-speaking users.

Also applies to: 87-87, 636-636, 640-642, 644-644, 650-650, 1105-1105, 1107-1107, 1128-1128, 1183-1184, 1223-1223, 1225-1225, 1233-1233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/de.json` at line 68, Several German translation keys are
empty which will render blank UI text; locate the entries for keys erYzJH,
FIKvUB, KI57sV, AZEGID, JkLHGw, CXI0EW, VQxsRK, W35dz6, 2YpSsF, HtTwhf, zoRFJV,
pm+3nT, 0Q1jqR, 8s+sTd, 495/Ut and lxHUeO and replace the empty string values
with appropriate German translations (matching the intent of their English
originals for mobile site features, app ID validation, and tracking text);
ensure formatting/escaping matches the surrounding JSON, run the i18n/lint or
build checks to validate there are no missing keys or formatting errors, and
include meaningful, context-aware German phrases for each key.

Comment thread client/messages/es.json
"tL4WC1": "Cargando más...",
"HPx76n": "Todos los elementos cargados",
"QkTyho": "Barra lateral de Rybbit",
"erYzJH": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace empty translation values to avoid blank UI text.

These newly introduced keys are set to "". In most i18n runtimes that results in blank labels/messages instead of fallback text, which breaks parts of the Spanish UI flow. Please provide translations now or remove these keys until translations are ready so fallback can work.

Also applies to: 87-87, 636-636, 640-642, 644-644, 650-650, 1105-1105, 1107-1107, 1128-1128, 1183-1184, 1223-1223, 1225-1225, 1233-1233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/es.json` at line 68, Several i18n keys (e.g., "erYzJH" and
the other keys listed at lines 87, 636, 640-642, 644, 650, 1105, 1107, 1128,
1183-1184, 1223, 1225, 1233) are set to empty strings which yields blank UI
text; for each such key either provide the proper Spanish translation string or
remove the key entirely from client/messages/es.json until a translation is
available so the i18n runtime can fall back to the default locale—update the
values for keys like "erYzJH" (and the others referenced) with their Spanish
text or delete those entries to avoid blank labels.

Comment thread client/messages/fr.json
"tL4WC1": "Chargement en cours...",
"HPx76n": "Tous les éléments chargés",
"QkTyho": "Barre latérale Rybbit",
"erYzJH": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not ship empty-string translations in French locale.

The new keys on these lines are empty (""), which will surface as missing/blank UI copy and can suppress fallback behavior. Please translate them or temporarily omit these keys until translations are available.

Also applies to: 87-87, 636-636, 640-642, 644-644, 650-650, 1105-1105, 1107-1107, 1128-1128, 1183-1184, 1223-1223, 1225-1225, 1233-1233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/fr.json` at line 68, The JSON contains empty-string
translations (e.g., key "erYzJH" and the other listed keys at lines mentioned)
which must not be shipped; either provide proper French translations for each
listed key (replace "" with the translated string) or remove/omit the keys
temporarily from client/messages/fr.json until translations are available, then
run the i18n build/validation step to ensure no empty values remain;
specifically update the keys "erYzJH" and the other keys called out (lines 87,
636, 640-642, 644, 650, 1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) so none
have empty values.

Comment thread client/messages/it.json
"tL4WC1": "Caricamento in corso...",
"HPx76n": "Tutti gli elementi caricati",
"QkTyho": "Barra Laterale Rybbit",
"erYzJH": "",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fill or remove empty Italian translation entries before merge.

These keys currently map to "", which can render blank controls/messages and weaken locale fallback behavior. Please add proper Italian copy (preferred) or remove the keys until translation is ready.

Also applies to: 87-87, 636-636, 640-642, 644-644, 650-650, 1105-1105, 1107-1107, 1128-1128, 1183-1184, 1223-1223, 1225-1225, 1233-1233

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/messages/it.json` at line 68, Several Italian translation entries are
empty (e.g., the key "erYzJH" and also keys at positions corresponding to the
other empty entries listed); either provide proper Italian copy for each empty
JSON key or remove the key entirely so the app falls back to the default locale.
Update the JSON keys "erYzJH" and the other empty keys (the ones reported at 87,
636, 640-642, 644, 650, 1105, 1107, 1128, 1183-1184, 1223, 1225, 1233) with
valid Italian strings or delete those key entries to avoid rendering blank UI
text.

Comment thread react-native/index.js
@@ -0,0 +1,427 @@
"use strict";

const SDK_VERSION = "0.1.0";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Keep SDK version in sync with package.json.

The SDK_VERSION constant is hardcoded and will drift from package.json version field unless manually updated on every release.

♻️ Recommended fix: Import version from package.json
-const SDK_VERSION = "0.1.0";
+const { version: SDK_VERSION } = require("./package.json");

This ensures the version is always consistent with the published package.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react-native/index.js` at line 3, The SDK_VERSION constant in
react-native/index.js is hardcoded and can drift from package.json; replace the
literal SDK_VERSION with the package version by importing the version field from
package.json (e.g., require or import the JSON and use its .version) and assign
that to SDK_VERSION so it always matches package.json; update any references to
SDK_VERSION to use the new imported value.

Comment thread react-native/index.js
Comment on lines +204 to +227
createBasePayload(context) {
this.ensureInitialized();
const screenSize = getScreenSize();
const appIdentifier = context?.appIdentifier || this.config.appIdentifier || this.config.siteId;

const payload = {
site_id: this.config.siteId,
anonymous_id: this.anonymousId,
hostname: appIdentifier,
pathname: asPathname(context?.pathname || context?.screen || "/"),
querystring: context?.querystring || "",
screenWidth: screenSize.screenWidth,
screenHeight: screenSize.screenHeight,
language: context?.language || getLanguage(),
page_title: context?.title || context?.screen || "",
referrer: context?.referrer || "",
user_agent: context?.userAgent || getUserAgent(this.config.appVersion),
};

if (this.userId) payload.user_id = this.userId;
if (this.config.tag) payload.tag = this.config.tag;

return payload;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consider requiring appIdentifier for mobile site tracking.

Line 207 falls back to this.config.siteId when appIdentifier is missing, then uses it as the hostname field (line 212). For mobile applications, using a numeric site ID as the hostname is semantically incorrect and could lead to confusing analytics data.

Mobile sites should require a proper app identifier (e.g., com.example.app or bundle ID). Consider validating that appIdentifier is set during init() or at the server layer that distinguishes mobile from web sites.

💡 Suggested validation in init()
 async init(config) {
   if (!config || !config.analyticsHost || !config.siteId) {
     throw new Error("analyticsHost and siteId are required");
   }
+  if (!config.appIdentifier && !config.bundleId) {
+    throw new Error("appIdentifier or bundleId is required for React Native SDK");
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@react-native/index.js` around lines 204 - 227, createBasePayload currently
falls back to this.config.siteId for the hostname which can put numeric site IDs
into the hostname field; enforce a proper app identifier by (1) changing init()
to validate that this.config.appIdentifier (or context.appIdentifier) is present
for mobile/React Native usage and throw or log a clear error if missing, and (2)
updating createBasePayload to use only context.appIdentifier ||
this.config.appIdentifier for hostname (do not fallback to this.config.siteId);
reference createBasePayload and init (or the module initialization path) and
update any docs/tests that construct the client so mobile integrations supply a
bundle/app identifier like com.example.app.

Comment on lines +70 to +80
const appIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/;
if (siteType === "web" && !domainRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid domain format. Must be a valid domain like example.com or sub.example.com",
});
}
if (siteType === "mobile" && !appIdentifierRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid app identifier. Use a bundle/package identifier like com.example.app",
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Tighten mobile app identifier validation to match bundle/package format.

Line 70 currently accepts many invalid identifiers (e.g., single-token values like myapp or trailing-dot variants) despite the error message requiring a bundle/package-style identifier. This can persist invalid mobile site identifiers.

Suggested fix
-  const appIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/;
+  // Require dot-separated bundle/package style (e.g. com.example.app)
+  const appIdentifierRegex =
+    /^(?=.{3,253}$)[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*(?:\.[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*)+$/;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const appIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/;
if (siteType === "web" && !domainRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid domain format. Must be a valid domain like example.com or sub.example.com",
});
}
if (siteType === "mobile" && !appIdentifierRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid app identifier. Use a bundle/package identifier like com.example.app",
});
}
const appIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/;
// Require dot-separated bundle/package style (e.g. com.example.app)
const appIdentifierRegex =
/^(?=.{3,253}$)[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*(?:\.[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*)+$/;
if (siteType === "web" && !domainRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid domain format. Must be a valid domain like example.com or sub.example.com",
});
}
if (siteType === "mobile" && !appIdentifierRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid app identifier. Use a bundle/package identifier like com.example.app",
});
}
Suggested change
const appIdentifierRegex = /^[A-Za-z0-9][A-Za-z0-9._-]{0,252}$/;
if (siteType === "web" && !domainRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid domain format. Must be a valid domain like example.com or sub.example.com",
});
}
if (siteType === "mobile" && !appIdentifierRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid app identifier. Use a bundle/package identifier like com.example.app",
});
}
// Require dot-separated bundle/package style (e.g. com.example.app)
const appIdentifierRegex =
/^(?=.{3,253}$)[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*(?:\.[A-Za-z0-9]+(?:[._-][A-Za-z0-9]+)*)+$/;
if (siteType === "web" && !domainRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid domain format. Must be a valid domain like example.com or sub.example.com",
});
}
if (siteType === "mobile" && !appIdentifierRegex.test(cleanedDomain)) {
return reply.status(400).send({
error: "Invalid app identifier. Use a bundle/package identifier like com.example.app",
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/api/sites/addSite.ts` around lines 70 - 80, The current
appIdentifierRegex is too permissive and allows single-token or trailing-dot
values; update the regex used in the siteType === "mobile" validation (symbol:
appIdentifierRegex and variable cleanedDomain in addSite.ts) to enforce
bundle/package format: require at least two dot-separated components, no
leading/trailing/consecutive dots, each component starting with a letter and
containing only letters, digits or underscores/hyphens as allowed (e.g. replace
with something like ^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)+$), then
use that regex in the existing if (siteType === "mobile" &&
!appIdentifierRegex.test(cleanedDomain)) branch and update the error message to
mention the required dot-separated bundle identifier format.

Comment on lines +87 to +88
const nextDomain = updateData.domain ?? site.domain;
const cleanedDomain = nextDomain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard nullish stored domains before calling replace().

site.domain can still be nullish for existing rows, and this normalization now runs on every update before the later domain/type guard. Line 88 will throw on a valid PATCH that only changes an unrelated field, turning it into a 500.

Suggested fix
-    const nextDomain = updateData.domain ?? site.domain;
+    const nextDomain = updateData.domain ?? site.domain ?? "";
     const cleanedDomain = nextDomain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const nextDomain = updateData.domain ?? site.domain;
const cleanedDomain = nextDomain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
const nextDomain = updateData.domain ?? site.domain ?? "";
const cleanedDomain = nextDomain.replace(/^https?:\/\//, "").replace(/\/+$/, "");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/api/sites/updateSiteConfig.ts` around lines 87 - 88, The code uses
nextDomain = updateData.domain ?? site.domain and immediately calls replace() on
it, which can throw when site.domain is nullish; update the logic around
nextDomain/cleanedDomain in updateSiteConfig.ts to guard for null/undefined
before calling replace(): compute nextDomain as before, then if nextDomain is
nullish skip normalization (e.g., leave cleanedDomain undefined or null) or
normalize via a safe fallback like (nextDomain ?? "") before replace, and ensure
the later domain/type validation still rejects absent domains as intended;
update references to nextDomain and cleanedDomain accordingly so unrelated
PATCHes don't hit replace() on nullish site.domain.

Comment on lines +70 to +74
async generateUserIdFromClientId(clientId: string, siteId: number): Promise<string> {
const config = await siteConfig.getConfig(siteId);
const salt = config?.saltUserIds ? this.getDailySalt() : "";

return crypto.createHash("sha256").update(`${siteId}:${clientId}:${salt}`).digest("hex").substring(0, 12);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep client-derived IDs stable across days.

Applying getDailySalt() here makes the same anonymous_id hash differently each UTC day when saltUserIds is enabled. That breaks the new identify flow across day boundaries: yesterday's anonymous events are stored under one hash, today's identify call computes another, and the backfill can never stitch them together. Daily rotation makes sense for the IP/UA fallback, but not for a client-supplied stable ID.

Suggested fix
   async generateUserIdFromClientId(clientId: string, siteId: number): Promise<string> {
-    const config = await siteConfig.getConfig(siteId);
-    const salt = config?.saltUserIds ? this.getDailySalt() : "";
-
-    return crypto.createHash("sha256").update(`${siteId}:${clientId}:${salt}`).digest("hex").substring(0, 12);
+    return crypto.createHash("sha256").update(`${siteId}:${clientId}`).digest("hex").substring(0, 12);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/src/services/userId/userIdService.ts` around lines 70 - 74, The
generateUserIdFromClientId function is using getDailySalt(), causing
client-derived anonymous_id hashes to change each UTC day; replace the
daily-rotating salt with a stable site-specific salt so the same clientId always
hashes to the same user id across days. Update generateUserIdFromClientId to use
a persistent salt from the site config (e.g., a config.userIdSalt or
config.salt) or an empty string when saltUserIds is false instead of calling
this.getDailySalt(), ensuring the hash input remains stable; adjust any
references to getDailySalt() here to a stable-salt accessor or field.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant