Skip to content

fix(system): restore and harden frontend product-analytics capture#16569

Open
flcarre wants to merge 2 commits into
developfrom
fix/frontend-posthog-telemetry
Open

fix(system): restore and harden frontend product-analytics capture#16569
flcarre wants to merge 2 commits into
developfrom
fix/frontend-posthog-telemetry

Conversation

@flcarre

@flcarre flcarre commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Why

A PostHog product-analytics audit (production instances, recent versions incl. Kestra Cloud) showed we are blind to most frontend errors and to part of the user-frustration signal:

  • $exception capture has been dark since ~May 18 — there is no global handler forwarding JS errors to PostHog, so we only ever recorded errors that happened to surface as a toast.
  • Vue component errors never reach window.onerror (Vue swallows them), so even basic render/lifecycle failures were invisible.
  • Rage clicks were never captured (autocapture: false disables the rageclick detector) — we only see fully-dead clicks.
  • Dead-clicks and Web Vitals were only flowing via PostHog server-side remote config, not owned by our code — exactly the kind of fragile toggle that silently dropped $exception.
  • The error analytics event was mislabeled: ErrorToast.vue fired a type: "ERROR" event for every toast variant, so success/info toasts (e.g. "copied logs to clipboard") were logged as error events, polluting the error stream.

What

  • composables/usePosthog.ts — make the signals we rely on code-owned and restore the missing ones in posthog.init: capture_exceptions, capture_dead_clicks, rageclick, capture_performance: { web_vitals: true }. autocapture stays false (no DOM autocapture).
  • utils/posthog.ts — add capturePosthogException() that routes through the existing lazy/queue-gated client and respects the anonymous-usage opt-out.
  • main.ts — wire app.config.errorHandler to capturePosthogException so Vue component errors are captured (with the Vue error info).
  • components/ErrorToast.vue — only emit the error analytics event for actual error variants (variant error or unset); success/info/warning toasts no longer count as errors.
  • tests/unit/utils/posthog.spec.ts — cover capturePosthogException (captures when enabled, no-ops when disabled).

Verification

  • npm run check:types ✅ (design-system + app + test)
  • npx oxlint on changed files ✅
  • npx vitest run tests/unit/utils/posthog.spec.ts ✅ (3/3)

Related

Surfaced by the same audit (tracked separately as UX work):

A PostHog audit showed $exception capture was dark, Vue component errors
and rage clicks were never recorded, and the error event was fired for
every toast variant (success/info logged as errors).

- usePosthog: enable capture_exceptions, capture_dead_clicks, rageclick
  and capture_performance.web_vitals in posthog.init (autocapture stays off)
  so the signals we rely on are code-owned, not dependent on remote config
- utils/posthog: add capturePosthogException through the lazy, opt-out-aware client
- main.ts: forward Vue app.config.errorHandler to PostHog
- ErrorToast: only emit the error event for actual error variants
- test: cover capturePosthogException

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

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

📄 OpenAPI Spec Changes

❌ Failed to generate EE OpenAPI spec (EE branch: develop).

/UserService.java:529: warning: [serial] serializable class UserAlreadyExistException has no definition of serialVersionUID
      public static class UserAlreadyExistException extends ConflictException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1034: warning: [serial] serializable class UnauthorizedInvitationException has no definition of serialVersionUID
      public static class UnauthorizedInvitationException extends KestraRuntimeException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1040: warning: [serial] serializable class BindingNotFoundException has no definition of serialVersionUID
      public static class BindingNotFoundException extends NotFoundException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1046: warning: [serial] serializable class RoleNotFoundException has no definition of serialVersionUID
      public static class RoleNotFoundException extends NotFoundException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1052: warning: [serial] serializable class GroupNotFoundException has no definition of serialVersionUID
      public static class GroupNotFoundException extends NotFoundException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1058: warning: [serial] serializable class UserNotInGroupException has no definition of serialVersionUID
      public static class UserNotInGroupException extends ConflictException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1064: warning: [serial] serializable class UserAlreadyInGroupException has no definition of serialVersionUID
      public static class UserAlreadyInGroupException extends ConflictException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1070: warning: [serial] serializable class GroupAlreadyExistsException has no definition of serialVersionUID
      public static class GroupAlreadyExistsException extends ConflictException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/RBACService.java:1076: warning: [serial] serializable class TenantAccessAlreadyExistsException has no definition of serialVersionUID
      public static class TenantAccessAlreadyExistsException extends ConflictException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/ServiceAccountService.java:263: warning: [serial] serializable class ServiceAccountForbiddenException has no definition of serialVersionUID
      public static class ServiceAccountForbiddenException extends KestraRuntimeException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/services/ServiceAccountService.java:269: warning: [serial] serializable class ServiceAccountFoundException has no definition of serialVersionUID
      public static class ServiceAccountFoundException extends NotFoundException {
                    ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/license/internals/MaxTenantsExceededException.java:5: warning: [serial] serializable class MaxTenantsExceededException has no definition of serialVersionUID
  public class MaxTenantsExceededException extends KestraLicenseException {
         ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/license/internals/MaxWorkerGroupExceededException.java:5: warning: [serial] serializable class MaxWorkerGroupExceededException has no definition of serialVersionUID
  public class MaxWorkerGroupExceededException extends KestraLicenseException {
         ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/plugin/ee/core/log/PurgeAuditLogs.java:39: warning: [text-blocks] trailing white space will be removed
              code = """
                     ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/runners/RunContextInitializer.java:36: warning: [unchecked] unchecked conversion
                      newOutputs.put(key, rehydrateOutputs(map));
                                                           ^
    required: Map<String,Object>
    found:    Map
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/plugins/EEPluginRegistry.java:126: warning: [rawtypes] found raw type: Class
                  registered.allClass().stream().map(Class::getName)
                                                     ^
    missing type arguments for generic class Class<T>
    where T is a type-variable:
      T extends Object declared in class Class
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/runners/RunContextInitializer.java:30: warning: [rawtypes] found raw type: Map
              if (value instanceof Map map) {
                                   ^
    missing type arguments for generic class Map<K,V>
    where K,V are type-variables:
      K extends Object declared in interface Map
      V extends Object declared in interface Map
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/plugin/ee/assets/File.java:37: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          this.setSystem(system);
                        ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/plugin/ee/assets/VM.java:38: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          this.setProvider(provider);
                          ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/plugin/ee/assets/Dataset.java:38: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          this.setSystem(system);
                        ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/plugin/ee/assets/Table.java:39: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          this.setSystem(system);
                        ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/apps/internals/DefaultAppContext.java:68: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          Optional.ofNullable(context).ifPresent(ctx -> ctx.inject(this));
                                                 ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/killswitch/EEKillSwitchService.java:49: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          reloadKillSwitches();
                            ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/plugins/EEPluginRegistry.java:64: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          init();
              ^
  /home/runner/work/kestra-ee/kestra-ee/core-ee/src/main/java/io/kestra/ee/plugins/RemotePluginManager.java:119: warning: [this-escape] possible 'this' escape before subclass is fully initialized
          this.queueSubscriber = clusterEventQueue.subscriber().subscribe(either ->
                                                                          ^
  Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
  1 error
  59 warnings

* Try:
> Check your code and dependencies to fix the compilation error(s)
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
==============================================================================

2: Task failed with an exception.
-----------
* What went wrong:
Execution failed for task ':ui-ee:assembleFrontend'.
> Process 'command '/home/runner/work/kestra-ee/kestra-ee/ui-ee/.gradle/nodejs/node-v22.18.0-linux-x64/bin/npm'' finished with non-zero exit value 1

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights from a Build Scan (powered by Develocity).
> Get more help at https://help.gradle.org.
==============================================================================

Deprecated Gradle features were used in this build, making it incompatible with Gradle 10.

You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.

For more on this, please refer to https://docs.gradle.org/9.4.1/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.

BUILD FAILED in 4m 2s
13 actionable tasks: 11 executed, 2 from cache

🐋 Docker image

ghcr.io/kestra-io/kestra-pr:16569
docker run --pull=always --rm -it -p 8080:8080 --user=root -v /var/run/docker.sock:/var/run/docker.sock -v /tmp:/tmp ghcr.io/kestra-io/kestra-pr:16569 server local

…sking

rageclick: true is a no-op when autocapture: false — the rageclick detector
in posthog-js relies on the autocapture click listener. Confirmed by 90 days
of production data showing zero $rageclick events.

Switch to a restricted autocapture config (click events only, a/button
elements only) and add mask_all_text: true to prevent capturing element
text content (flow names, namespace names, labels). This keeps rageclick
detection functionally equivalent to the existing $dead_click capture, with
the same GDPR exposure profile.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To review

Development

Successfully merging this pull request may close these issues.

1 participant