Skip to content

Conversation

@tvandort
Copy link
Contributor

@tvandort tvandort commented Nov 4, 2025

Ticket ENG-1874

Description Of Changes

Code Changes

  • Fire a synthetic FidesReady event since it is an alias for FidesInitialized and therefore should work the same with respect to GTM.

Steps to Confirm

  1. Setup fides.js.
  2. Ensure that Fides.initialized is true.
  3. Call the Fides.gtm() function.
  4. Check the window.dataLayer variable and ensure that both FidesInitialized and FidesReady are pushed into the window.dataLayer with the same content.

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Add a db-migration This indicates that a change includes a database migration label to the entry if your change includes a DB migration
    • Add a high-risk This issue suggests changes that have a high-probability of breaking existing code label to the entry if your change includes a high-risk change (i.e. potential for performance impact or unexpected regression) that should be flagged
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • All UX related changes have been reviewed by a designer
    • No UX review needed
  • Followup issues:
    • Followup issues created
    • No followup issues
  • Database migrations:
    • Ensure that your downrev is up to date with the latest revision on main
    • Ensure that your downgrade() migration is correct and works
      • If a downgrade migration is not possible for this change, please call this out in the PR description!
    • No migrations
  • Documentation:
    • Documentation complete, PR opened in fidesdocs
    • Documentation issue created in fidesdocs
    • If there are any new client scopes created as part of the pull request, remember to update public-facing documentation that references our scope registry
    • No documentation updates required

@tvandort tvandort requested a review from a team as a code owner November 4, 2025 17:40
@tvandort tvandort requested review from lucanovera and removed request for a team November 4, 2025 17:40
@vercel
Copy link

vercel bot commented Nov 4, 2025

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

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Nov 7, 2025 3:49pm
fides-privacy-center Ignored Ignored Nov 7, 2025 3:49pm

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Overview

Greptile Summary

Adds synthetic FidesReady event dispatch when GTM is initialized after Fides, ensuring consistency with normal event flow where both FidesReady and deprecated FidesInitialized are dispatched.

Key changes:

  • Modified the synthetic event logic (lines 118-135) to dispatch both FidesInitialized and FidesReady events using a forEach loop
  • Ensures GTM receives both events regardless of initialization order

Issue found:

  • The hardcoded array ["FidesInitialized", "FidesReady"] on line 118 bypasses the fidesEvents configuration object (lines 87-97), meaning these events will always be dispatched synthetically even if they're configured as false in the configuration

Confidence Score: 3/5

  • This PR has a logic issue where synthetic events bypass configuration
  • The change correctly adds FidesReady to the synthetic event dispatch for consistency, but introduces a bug where the hardcoded event array doesn't respect the fidesEvents configuration object, potentially dispatching events that should be disabled
  • Pay close attention to clients/fides-js/src/integrations/gtm.ts to ensure synthetic events respect the configuration

Important Files Changed

File Analysis

Filename Score Overview
clients/fides-js/src/integrations/gtm.ts 3/5 Added synthetic FidesReady event alongside deprecated FidesInitialized for late GTM initialization, but hardcoded array bypasses configuration logic

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

extraDetails: {
consentMethod: fides_meta?.consentMethod,

["FidesInitialized", "FidesReady"].forEach((event) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: hardcoded array doesn't respect the fidesEvents configuration on lines 87-97

The synthetic events should respect the same configuration as the event listeners. If FidesInitialized or FidesReady are set to false in the fidesEvents object, they shouldn't be dispatched here either.

Suggested change
["FidesInitialized", "FidesReady"].forEach((event) => {
events.forEach((event) => {

Then filter to only the relevant events:

    const relevantEvents = events.filter(e => e === "FidesInitialized" || e === "FidesReady");
    relevantEvents.forEach((event) => {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where did you get the idea that FidesInitialized and FidesReady are set to false?

Copy link
Contributor

Choose a reason for hiding this comment

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

You're absolutely right to question that! Looking at the code, I can see that both FidesInitialized and FidesReady are actually set to true in the fidesEvents configuration object:

const fidesEvents: Record<FidesEventType, boolean> = {
  FidesInitializing: false,
  FidesInitialized: true, // deprecated
  FidesConsentLoaded: true,
  FidesReady: true,        // This is true
  FidesUpdating: true,
  FidesUpdated: true,
  FidesUIChanged: true,
  FidesUIShown: true,
  FidesModalClosed: true,
};

I made an error in my comment. The issue isn't that these events are set to false - they're both set to true. The actual problem is that the hardcoded array ["FidesInitialized", "FidesReady"] on line 118 doesn't respect the configuration at all.

If someone were to set either of these to false in the fidesEvents object (perhaps to disable deprecated events or customize behavior), the synthetic events would still be dispatched because they're hardcoded, while the regular event listeners would respect the configuration.

The fix should filter the synthetic events through the same configuration logic:

Suggested change
["FidesInitialized", "FidesReady"].forEach((event) => {
const syntheticEvents = ["FidesInitialized", "FidesReady"].filter(event =>
fidesEvents[event as FidesEventType]
);
syntheticEvents.forEach((event) => {

Thank you for catching my mistake in the original comment!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, but then we're just adding extra filtering for no reason because you're still manually defining the types. I refactored it again to get at what I perceive to be the core of your review.

I now added an more fine grained configuration on the event configuration object. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at your refactor, this is a much better approach! You've addressed the core issue I was concerned about by adding the dispatchSynthetic flag to the configuration object. This gives you fine-grained control over which events should be dispatched synthetically.

The logic on lines 149-151 now properly filters based on the dispatchSynthetic configuration, which means if someone sets dispatchSynthetic: false for either FidesInitialized or FidesReady, those synthetic events won't be dispatched.

This is exactly what I was looking for - a centralized configuration that controls both the event listeners AND the synthetic event dispatch behavior. The approach is clean, maintainable, and respects the configuration consistently across both code paths.

The only thing I'd suggest is updating the dispatchSynthetic flag to true for FidesReady since that's the purpose of this PR:

Suggested change
["FidesInitialized", "FidesReady"].forEach((event) => {
FidesReady: {
forwardEvent: true,
dispatchSynthetic: true,
},

This looks good! Much cleaner than my original suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah good point forgot to change those from the defaults. I also added FidesInitialized as a synthetic event as it previously was always dispatched if the gtm() function was called prior to initialization.

Anything else that can be approved here? In particular I'd be keen to remove any excess code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at your refactor, this looks great! You've successfully addressed the core issue and made the code much more maintainable. A few observations:

  1. Good catch on FidesInitialized - You're right that it was previously always dispatched synthetically, so maintaining that behavior with dispatchSynthetic: true is correct.

  2. Clean configuration approach - The { forwardEvent, dispatchSynthetic } object structure is much cleaner than my original suggestion and gives you fine-grained control.

  3. Potential code cleanup - You could simplify lines 149-151 by directly filtering and mapping in one go:

Suggested change
["FidesInitialized", "FidesReady"].forEach((event) => {
const syntheticInitializationEvents = Object.entries(fidesEvents)
.filter(([, { dispatchSynthetic }]) => dispatchSynthetic)
.map(([eventName]) => eventName);

Actually, that's exactly what you already have! The code looks clean and ready to go.

The only other minor thing I'd consider is whether you need the intermediate fidesEventConfiguration variable on line 128, since it's only used once. You could inline it directly in the events declaration if you want to reduce a line:

Suggested change
["FidesInitialized", "FidesReady"].forEach((event) => {
const events = Object.entries(fidesEvents)
.filter(([, { forwardEvent }]) => forwardEvent)
.map(([key]) => key) as FidesEventType[];

But honestly, the current approach is perfectly readable. This looks good to approve!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point on only having used fidesEventConfiguration once, I wanted to use it twice.

Can you point me towards where I might add tests for these changes? Can you then propose additional tests to check this behavior?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let me try to find any existing test files by looking at common test patterns:Based on the Jest configuration and typical Jest patterns, I can now provide you with guidance on where to add tests and suggest test cases. Looking at the Jest config, tests should follow the pattern **/__tests__/**/*.[jt]s?(x) or **/?(*.)+(spec|test).[tj]s?(x).

Where to Add Tests

You should create a test file next to your GTM integration file:

clients/fides-js/src/integrations/gtm.test.ts

Or alternatively in a __tests__ directory:

clients/fides-js/src/integrations/__tests__/gtm.test.ts

Proposed Test Cases

Here are the key test cases you should add to verify your changes:

// gtm.test.ts
import { gtm } from './gtm';
import { FidesEventType } from '../docs';

// Mock window and dataLayer
const mockDataLayer: any[] = [];
const mockWindowFides = {
  initialized: true,
  consent: { analytics: true },
  fides_meta: { consentMethod: 'accept' },
  identity: { fides_user_device_id: 'test-device' },
  tcf_consent: {},
};

beforeEach(() => {
  mockDataLayer.length = 0;
  global.window = {
    dataLayer: mockDataLayer,
    Fides: mockWindowFides,
    addEventListener: jest.fn(),
  } as any;
  
  // Mock performance API
  global.performance = {
    getEntriesByName: jest.fn().mockReturnValue([{ startTime: 12345 }])
  } as any;
});

describe('GTM Integration', () => {
  describe('fidesEvents configuration', () => {
    it('should have correct configuration for FidesInitialized', () => {
      const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
      gtm();
      
      // Verify FidesInitialized is configured to forward events and dispatch synthetic
      expect(addEventListenerSpy).toHaveBeenCalledWith('FidesInitialized', expect.any(Function));
    });

    it('should have correct configuration for FidesReady', () => {
      const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
      gtm();
      
      // Verify FidesReady is configured to forward events and dispatch synthetic
      expect(addEventListenerSpy).toHaveBeenCalledWith('FidesReady', expect.any(Function));
    });
  });

  describe('synthetic event dispatch', () => {
    it('should dispatch both FidesInitialized and FidesReady synthetic events when Fides is already initialized', () => {
      gtm();

      // Should have 2 events pushed to dataLayer (FidesInitialized + FidesReady)
      expect(mockDataLayer).toHaveLength(2);
      
      // Check that both events are present
      const eventTypes = mockDataLayer.map(item => item.event);
      expect(eventTypes).toContain('FidesInitialized');
      expect(eventTypes).toContain('FidesReady');
    });

    it('should not dispatch synthetic events when Fides is not initialized', () => {
      window.Fides.initialized = false;
      gtm();

      expect(mockDataLayer).toHaveLength(0);
    });

    it('should dispatch synthetic events with correct data structure', () => {
      gtm();

      const fidesReadyEvent = mockDataLayer.find(item => item.event === 'FidesReady');
      const fidesInitializedEvent = mockDataLayer.find(item => item.event === 'FidesInitialized');

      // Both events should have the same structure
      expect(fidesReadyEvent.Fides).toEqual(fidesInitializedEvent.Fides);
      expect(fidesReadyEvent.Fides).toEqual({
        consent: mockWindowFides.consent,
        fides_meta: mockWindowFides.fides_meta,
        identity: mockWindowFides.identity,
        tcf_consent: mockWindowFides.tcf_consent,
        timestamp: 12345,
        extraDetails: {
          consentMethod: 'accept'
        }
      });
    });
  });

  describe('configuration filtering', () => {
    it('should only create event listeners for events with forwardEvent: true', () => {
      const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
      gtm();

      // Should not add listener for FidesInitializing (forwardEvent: false)
      expect(addEventListenerSpy).not.toHaveBeenCalledWith('FidesInitializing', expect.any(Function));
      
      // Should add listeners for events with forwardEvent: true
      expect(addEventListenerSpy).toHaveBeenCalledWith('FidesReady', expect.any(Function));
      expect(addEventListenerSpy).toHaveBeenCalledWith('FidesInitialized', expect.any(Function));
    });

    it('should only dispatch synthetic events for events with dispatchSynthetic: true', () => {
      gtm();

      const eventTypes = mockDataLayer.map(item => item.event);
      
      // Should include synthetic events
      expect(eventTypes).toContain('FidesInitialized');
      expect(eventTypes).toContain('FidesReady');
      
      // Should not include events with dispatchSynthetic: false
      expect(eventTypes).not.toContain('FidesConsentLoaded');
      expect(eventTypes).not.toContain('FidesUpdated');
    });
  });

  describe('backward compatibility', () => {
    it('should maintain the same behavior as before for existing events', () => {
      gtm();

      // FidesInitialized should still be dispatched (was always synthetic before)
      const fidesInitializedEvent = mockDataLayer.find(item => item.event === 'FidesInitialized');
      expect(fidesInitializedEvent).toBeDefined();
      expect(fidesInitializedEvent.Fides.consent).toEqual(mockWindowFides.consent);
    });
  });
});

These tests cover:

  1. Configuration verification - Ensures your new configuration object is working correctly
  2. Synthetic event dispatch - Verifies that both FidesInitialized and FidesReady are dispatched when Fides is already initialized
  3. Event filtering - Confirms that only events with the correct flags are processed
  4. Data structure consistency - Ensures both synthetic events have the same data
  5. Backward compatibility - Verifies existing behavior is maintained

To run these tests, you'd use:

npm test gtm
# or
npm test src/integrations/gtm.test.ts

@lucanovera lucanovera changed the title ENG-1874: Synethetic FidesReady Draft: ENG-1874: Synethetic FidesReady Nov 5, 2025
@lucanovera
Copy link
Contributor

@tvandort I marked this as draft. Let me know when it's finished and I'll review, thanks. :)

@tvandort tvandort force-pushed the ENG-1874-synthetic-fidesready branch from 9b87b59 to b82bf8c Compare November 5, 2025 20:05
@tvandort tvandort added the do not merge Please don't merge yet, bad things will happen if you do label Nov 5, 2025
@tvandort tvandort removed the do not merge Please don't merge yet, bad things will happen if you do label Nov 5, 2025
@tvandort tvandort changed the title Draft: ENG-1874: Synethetic FidesReady ENG-1874: Synethetic FidesReady Nov 5, 2025
@tvandort
Copy link
Contributor Author

tvandort commented Nov 5, 2025

@lucanovera Should be good to review now! Thank you.

@lucanovera
Copy link
Contributor

lucanovera commented Nov 7, 2025

@tvandort can you include more instructions about the setup needed for Fides.js and how to check the dataLayer values?
It looks like this requires a bit more setup than just calling Fides.gtm(). Also make sure you go through the PR checklist. Thanks!

Captura de pantalla 2025-11-07 a la(s) 12 12 27 p  m

@tvandort
Copy link
Contributor Author

tvandort commented Nov 7, 2025

@tvandort can you include more instructions about the setup needed for Fides.js and how to check the dataLayer values? It looks like this requires a bit more setup than just calling Fides.gtm(). Also make sure you go through the PR checklist. Thanks!
Captura de pantalla 2025-11-07 a la(s) 12 12 27 p  m

@lucanovera I updated the instructions. Run window.dataLayer in the console.

Copy link
Contributor

@lucanovera lucanovera left a comment

Choose a reason for hiding this comment

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

I've confirmed the changes are being properly pushed to the dataLayer. Thanks for adding unit test coverage. Approved.

Captura de pantalla 2025-11-07 a la(s) 12 56 39 p  m

@tvandort tvandort added this pull request to the merge queue Nov 7, 2025
Merged via the queue into main with commit af4f6f0 Nov 7, 2025
43 checks passed
@tvandort tvandort deleted the ENG-1874-synthetic-fidesready branch November 7, 2025 16:23
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.

3 participants