From 3c0186a7176176eb996c73bc87a559d08246e8b4 Mon Sep 17 00:00:00 2001
From: Niko Belokolodov <nbelokolodov@gitlab.com>
Date: Thu, 13 Mar 2025 06:28:14 +1300
Subject: [PATCH] Trigger Sentry when tracking called with incorrect event name

---
 .../tracking/dispatch_snowplow_event.js       |  3 ++
 app/assets/javascripts/tracking/utils.js      |  7 ++++
 spec/frontend/tracking/utils_spec.js          | 32 +++++++++++++++++++
 3 files changed, 42 insertions(+)

diff --git a/app/assets/javascripts/tracking/dispatch_snowplow_event.js b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
index 91512292eb68f..4ae88268c4be2 100644
--- a/app/assets/javascripts/tracking/dispatch_snowplow_event.js
+++ b/app/assets/javascripts/tracking/dispatch_snowplow_event.js
@@ -1,5 +1,6 @@
 import * as Sentry from '~/sentry/sentry_browser_wrapper';
 import getStandardContext from './get_standard_context';
+import { validateEvent } from './utils';
 
 export function dispatchSnowplowEvent(
   category = document.body.dataset.page,
@@ -11,6 +12,8 @@ export function dispatchSnowplowEvent(
     throw new Error('Tracking: no category provided for tracking.');
   }
 
+  validateEvent(action);
+
   const { label, property, extra = {} } = data;
   let { value } = data;
 
diff --git a/app/assets/javascripts/tracking/utils.js b/app/assets/javascripts/tracking/utils.js
index 6493147672ec2..bfb767260f351 100644
--- a/app/assets/javascripts/tracking/utils.js
+++ b/app/assets/javascripts/tracking/utils.js
@@ -1,6 +1,7 @@
 import { omitBy, isUndefined } from 'lodash';
 import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
 import { getExperimentData } from '~/experimentation/utils';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
 import {
   ACTION_ATTR_SELECTOR,
   LOAD_ACTION_ATTR_SELECTOR,
@@ -189,6 +190,12 @@ export const validateAdditionalProperties = (additionalProperties) => {
   });
 };
 
+export const validateEvent = (event) => {
+  if (event && /\s/.test(event)) {
+    Sentry.captureException(new Error(`Event name should not contain whitespace: ${event}`));
+  }
+};
+
 function filterProperties(additionalProperties, predicate) {
   return Object.keys(additionalProperties).reduce((acc, key) => {
     if (predicate(key)) {
diff --git a/spec/frontend/tracking/utils_spec.js b/spec/frontend/tracking/utils_spec.js
index 91e0f958ef518..59dcab27afdd1 100644
--- a/spec/frontend/tracking/utils_spec.js
+++ b/spec/frontend/tracking/utils_spec.js
@@ -9,7 +9,9 @@ import {
   validateAdditionalProperties,
   getCustomAdditionalProperties,
   getBaseAdditionalProperties,
+  validateEvent,
 } from '~/tracking/utils';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
 import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
 import { REFERRER_TTL, URLS_CACHE_STORAGE_KEY } from '~/tracking/constants';
 import { TEST_HOST } from 'helpers/test_constants';
@@ -220,6 +222,36 @@ describe('~/tracking/utils', () => {
     });
   });
 
+  describe('validateEvent', () => {
+    let sentrySpy;
+
+    beforeEach(() => {
+      sentrySpy = jest.spyOn(Sentry, 'captureException');
+    });
+
+    afterEach(() => {
+      sentrySpy.mockRestore();
+    });
+
+    it('calls Sentry for event names with whitespace', () => {
+      validateEvent('event name');
+
+      expect(sentrySpy).toHaveBeenCalled();
+    });
+
+    it('does not call Sentry for event names eqaual to nil', () => {
+      validateEvent(null);
+
+      expect(sentrySpy).not.toHaveBeenCalled();
+    });
+
+    it('does not call Sentry for event names without whitespace', () => {
+      validateEvent('event-name');
+
+      expect(sentrySpy).not.toHaveBeenCalled();
+    });
+  });
+
   describe('getCustomAdditionalProperties', () => {
     it('returns only custom properties', () => {
       const additionalProperties = {
-- 
GitLab