Skip to content

fix: fully honor block-anonymous-tracking flag on client and server#41973

Open
salevine wants to merge 3 commits into
releasefrom
chore/15380/usage_info
Open

fix: fully honor block-anonymous-tracking flag on client and server#41973
salevine wants to merge 3 commits into
releasefrom
chore/15380/usage_info

Conversation

@salevine

@salevine salevine commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Makes the configure_block_event_tracking_for_anonymous_users feature flag completely turn off anonymous-user event tracking. Two gaps let anonymous events through when the flag was on:

Gap 1 — client license bypass (app/client/src/ce/sagas/userSagas.tsx)

shouldTrackUser short-circuited on an active license:

isAnonymous && (licenseActive || (telemetryOn && !featureFlag))

so licensed EE/cloud instances tracked anonymous users regardless of the flag. Removed the licenseActive bypass — the flag is now honored on all instances.

Gap 2 — ungated direct sendEvent path (AnalyticsServiceCEImpl)

The flag was only checked in sendObjectEvent. Direct callers of sendEvent(event, userId, props) with an anonymous userId bypassed it (and that path resolves the anonymous user to the client x-anonymous-user-id). Added a gate on the public sendEvent that blocks anonymous userIds before id resolution, and it fails closed if the flag state can't be resolved (drops the event, completes the chain). The existing sendObjectEvent check is kept (it governs the session-user/signup path, where sendEvent receives the new user's email rather than "anonymousUser"). sendObjectEvent now routes through the extracted private sendEventInternal to avoid a redundant (Redis-backed) flag check on the hot anonymous path (published-app page views / action executions).

Scope

  • Behavior is opt-in: the flag defaults to false, so existing instances are unchanged until an admin enables it.
  • Out of scope (intentional): the usage-pulse channel (/api/v1/usage-pulse) still emits the anonymous ID — that feeds billing/seat metering, not Segment analytics, and is treated separately.

Tests

  • Client — new userSagas.test.ts covering the shouldTrackUser matrix, including the regression guard that anonymous + telemetry-on + flag-on returns false on a licensed instance.
  • Server — new AnalyticsServiceCEImplTest cases: a direct anonymous sendEvent enqueues nothing when the flag is on, and fails closed (no enqueue, no error) when the flag check errors.

Ticket: #15380

Automation

/ok-to-test tags="@tag.All"

🔍 Cypress test results

Tip

🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉
Workflow run: https://github.com/appsmithorg/appsmith/actions/runs/29031827132
Commit: a75de43
Cypress dashboard.
Tags: @tag.All
Spec:


Thu, 09 Jul 2026 17:23:34 UTC

Communication

Should the DevRel and Marketing teams inform users about this change?

  • Yes
  • No

The configure_block_event_tracking_for_anonymous_users flag left two gaps
that let anonymous-user events through when it was on:

- Client (shouldTrackUser): an active license short-circuited the flag,
  so licensed EE/cloud instances always tracked anonymous users. Removed
  the licenseActive bypass so the flag is honored on all instances.
- Server (AnalyticsServiceCEImpl.sendEvent): the flag was only checked in
  sendObjectEvent. Direct sendEvent callers passing an anonymous userId
  bypassed it. Added a gate on the public sendEvent that blocks anonymous
  userIds before the x-anonymous-user-id is resolved, keeping the existing
  sendObjectEvent check for the session-user (signup) path. sendObjectEvent
  now routes through the extracted private sendEventInternal to avoid a
  redundant (Redis-backed) flag check on the hot anonymous path.

usage-pulse billing/seat metering is intentionally out of scope.

Adds unit tests: client shouldTrackUser matrix (incl. the license-bypass
regression guard) and a server sendEvent anonymous-block guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@salevine salevine added the ok-to-test Required label for CI label Jul 9, 2026
@github-actions github-actions Bot added the Bug Something isn't working label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Whoops! Looks like you're using an outdated method of running the Cypress suite.
Please check this doc to learn how to correct this!

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Anonymous-user analytics tracking now uses the configure_block_event_tracking_for_anonymous_users feature flag on both client and server. The client saga and tests were updated for the new tracking decision logic, and the server analytics service now blocks anonymous event emission when the flag is enabled or unresolved.

Changes

Anonymous User Analytics Gating

Layer / File(s) Summary
Client tracking decision
app/client/src/ce/sagas/userSagas.tsx, app/client/src/ce/sagas/userSagas.test.ts
shouldTrackUser now uses ANONYMOUS_USERNAME, takes a single feature-flag argument, and initTrackers passes the anonymous-user blocking flag after removing organization config access; tests cover the updated tracking outcomes.
Server anonymous event gating
app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java, app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java
sendEvent blocks anonymous events when the feature flag says to block or when flag resolution fails, sendObjectEvent reuses the evaluated block state, and tests assert anonymous events are not enqueued in both cases.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant initTrackers
  participant shouldTrackUser
  participant AnalyticsServiceCEImpl
  participant FeatureFlagService
  participant SegmentAnalytics

  initTrackers->>shouldTrackUser: currentUser, configure_block_event_tracking_for_anonymous_users
  shouldTrackUser-->>initTrackers: track or skip decision

  AnalyticsServiceCEImpl->>FeatureFlagService: check block flag for anonymous user
  FeatureFlagService-->>AnalyticsServiceCEImpl: flag result or error
  alt blocked or unresolved
    AnalyticsServiceCEImpl-->>AnalyticsServiceCEImpl: return Mono.empty()
  else allowed
    AnalyticsServiceCEImpl->>SegmentAnalytics: sendEventInternal / enqueue
  end
Loading

Poem

A flag now guards the anonymous trail,
On client and server, it tells the tale.
If blocked, the event drifts softly away,
If open, it travels on its way.
Tests stand watch with a steady eye,
And Segment stays quiet when replies say “deny.”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title is concise and accurately summarizes the main client/server anonymous-tracking fix.
Description check ✅ Passed The description includes motivation, scope, tests, automation, and a ticket reference; only minor template details are missing.
✨ 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 chore/15380/usage_info

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.

…S_USERNAME

Code-review follow-ups on the anonymous-tracking gate:

- sendEvent's anonymous gate now handles featureFlagService.check()
  errors with onErrorResume, failing closed (event dropped, chain
  completes) instead of propagating the error into previously
  fire-and-forget callers like GlobalExceptionHandler. Adds a
  regression test for the error path.
- Align gate style with repo convention: raw if (isBlocked) and plain
  Mono.empty() (Reactor forbids null onNext, so the defensive forms
  bought nothing).
- Client: use the existing ANONYMOUS_USERNAME constant from
  userConstants instead of the hardcoded "anonymousUser" literal in
  shouldTrackUser and its test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@salevine

salevine commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

/ok-to-test tags="@tag.All"

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Whoops! Looks like you're using an outdated method of running the Cypress suite.
Please check this doc to learn how to correct this!

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java (2)

55-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the ANONYMOUS_USERNAME constant instead of a hardcoded string.

The PR objectives note that the client was updated to use ANONYMOUS_USERNAME instead of a hardcoded "anonymousUser" string. The tests should reference the same server-side constant (if one exists) to avoid drift if the value ever changes.

Run the following script to check for an existing constant:

#!/bin/bash
# Description: Search for an ANONYMOUS_USERNAME or similar constant on the server side.
rg -n 'ANONYMOUS_USERNAME|ANONYMOUS_USER' --type java app/server/appsmith-server/src/main/java/com/appsmith/server/

Also applies to: 73-73

🤖 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
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java`
at line 55, The test is using a hardcoded anonymous username string instead of
the shared server constant, so update AnalyticsServiceCEImplTest to reference
the existing ANONYMOUS_USERNAME symbol wherever sendEvent is called with the
anonymous user. Check the server-side constant location first and use that value
consistently in the test methods so the assertions stay aligned with the
production contract.

41-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a positive test case for when the flag is disabled.

Both new tests cover the blocking paths (flag on, flag error). A third test where the flag resolves to false (don't block) and asserting that enqueue is called would confirm the gate works bidirectionally and strengthen confidence that the blocking tests aren't trivially passing.

🤖 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
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java`
around lines 41 - 77, Add a positive regression test in
AnalyticsServiceCEImplTest for the anonymous-user path when
FeatureFlagService.check(configure_block_event_tracking_for_anonymous_users)
resolves to false, so the gate does not short-circuit. Reuse
AnalyticsServiceCEImpl with a mocked Analytics and FeatureFlagService, call
sendEvent with an anonymous user, and verify the returned Mono completes and
analytics.enqueue is invoked once. This should sit alongside the existing
sendEvent_anonymousUserWithBlockFlagOn_doesNotEnqueueEvent and
sendEvent_anonymousUserWhenFlagCheckErrors_completesWithoutEnqueueing tests.
🤖 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.

Nitpick comments:
In
`@app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java`:
- Line 55: The test is using a hardcoded anonymous username string instead of
the shared server constant, so update AnalyticsServiceCEImplTest to reference
the existing ANONYMOUS_USERNAME symbol wherever sendEvent is called with the
anonymous user. Check the server-side constant location first and use that value
consistently in the test methods so the assertions stay aligned with the
production contract.
- Around line 41-77: Add a positive regression test in
AnalyticsServiceCEImplTest for the anonymous-user path when
FeatureFlagService.check(configure_block_event_tracking_for_anonymous_users)
resolves to false, so the gate does not short-circuit. Reuse
AnalyticsServiceCEImpl with a mocked Analytics and FeatureFlagService, call
sendEvent with an anonymous user, and verify the returned Mono completes and
analytics.enqueue is invoked once. This should sit alongside the existing
sendEvent_anonymousUserWithBlockFlagOn_doesNotEnqueueEvent and
sendEvent_anonymousUserWhenFlagCheckErrors_completesWithoutEnqueueing tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dc6fb33f-56ff-432f-8ded-d9f5dc444e0d

📥 Commits

Reviewing files that changed from the base of the PR and between 8708b67 and dbf8f56.

📒 Files selected for processing (4)
  • app/client/src/ce/sagas/userSagas.test.ts
  • app/client/src/ce/sagas/userSagas.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java
  • app/server/appsmith-server/src/test/java/com/appsmith/server/services/ce/AnalyticsServiceCEImplTest.java
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/client/src/ce/sagas/userSagas.test.ts
  • app/client/src/ce/sagas/userSagas.tsx
  • app/server/appsmith-server/src/main/java/com/appsmith/server/services/ce/AnalyticsServiceCEImpl.java

@salevine salevine added ok-to-test Required label for CI and removed ok-to-test Required label for CI labels Jul 9, 2026
@salevine salevine requested a review from subrata71 July 9, 2026 15:21

@subrata71 subrata71 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This blocks Segment initialization for anonymous users when configure_block_event_tracking_for_anonymous_users is enabled, which looks correct for analytics tracking.

But usage-pulse still depends on AnalyticsUtil.getAnonymousId() when enableTelemetry && segment.enabled is true. In this new state, segmentAnalytics exists but has no Segment user, so getAnonymousId() can return undefined, and /api/v1/usage-pulse may stop sending anonymousUserId.

Can we also update usage-pulse to fall back to its local FALLBACK_KEY/nanoid() ID when AnalyticsUtil.getAnonymousId() is unavailable? That would preserve the PR’s stated scope that usage-pulse remains separate from Segment analytics blocking. Also could you please verify how does the PR behave in the EE repo?

@salevine

Copy link
Copy Markdown
Contributor Author

Hardening: guard localStorage in the usage-pulse fallback id (commit b23ab45479)

Follow-up to the usage-pulse fallback change. getOrCreateFallbackAnonymousId() in app/client/src/usagePulse/utils.ts writes to localStorage, which can throw when storage is unavailable (private/incognito mode, quota exceeded, or storage disabled by policy). Because this helper is now reached on the telemetry-enabled anonymous path (not just the telemetry-disabled one), an unguarded throw would propagate up through getUsagePulsePayloadUsagePulse.sendPulse and drop the pulse entirely.

Fix: wrap the read/write in a try/catch. On failure we return a non-persisted per-call nanoid() so the pulse still carries an anonymousUserId instead of failing. The trade-off is that the id isn't stable across pulses in that degraded environment — acceptable, since the alternative is losing the pulse.

const getOrCreateFallbackAnonymousId = (): string => {
  try {
    let fallback = localStorage.getItem(FALLBACK_KEY);
    if (!fallback) {
      fallback = nanoid();
      localStorage.setItem(FALLBACK_KEY, fallback);
    }
    return fallback;
  } catch {
    // localStorage unavailable (private mode, quota, disabled) — return a
    // non-persisted per-call id so the pulse still carries an anonymousUserId.
    return nanoid();
  }
};

Covered by a new unit test (utils.test.ts) that mocks Storage.prototype.setItem to throw and asserts a non-empty anonymousUserId is still produced.

Verification: yarn jest src/usagePulse/utils.test.ts src/usagePulse/usagePulse.test.ts → 11/11 pass; yarn run check-types and yarn eslint on the changed files both clean.

@salevine

Copy link
Copy Markdown
Contributor Author

This blocks Segment initialization for anonymous users when configure_block_event_tracking_for_anonymous_users is enabled, which looks correct for analytics tracking.

But usage-pulse still depends on AnalyticsUtil.getAnonymousId() when enableTelemetry && segment.enabled is true. In this new state, segmentAnalytics exists but has no Segment user, so getAnonymousId() can return undefined, and /api/v1/usage-pulse may stop sending anonymousUserId.

Can we also update usage-pulse to fall back to its local FALLBACK_KEY/nanoid() ID when AnalyticsUtil.getAnonymousId() is unavailable? That would preserve the PR’s stated scope that usage-pulse remains separate from Segment analytics blocking. Also could you please verify how does the PR behave in the EE repo?

@subrata71 - I updated it to address this

@salevine salevine requested a review from subrata71 July 10, 2026 14:05
@subrata71

Copy link
Copy Markdown
Collaborator

This blocks Segment initialization for anonymous users when configure_block_event_tracking_for_anonymous_users is enabled, which looks correct for analytics tracking.
But usage-pulse still depends on AnalyticsUtil.getAnonymousId() when enableTelemetry && segment.enabled is true. In this new state, segmentAnalytics exists but has no Segment user, so getAnonymousId() can return undefined, and /api/v1/usage-pulse may stop sending anonymousUserId.
Can we also update usage-pulse to fall back to its local FALLBACK_KEY/nanoid() ID when AnalyticsUtil.getAnonymousId() is unavailable? That would preserve the PR’s stated scope that usage-pulse remains separate from Segment analytics blocking. Also could you please verify how does the PR behave in the EE repo?

@subrata71 - I updated it to address this

@salevine I don't see any recent changes which addresses this. Forgot to push maybe?

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

Labels

Bug Something isn't working ok-to-test Required label for CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants