-
Notifications
You must be signed in to change notification settings - Fork 36
feat(KFLUXUI-1048/49): Segment config and analytics initialisation for Active users telemetry #694
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
milantaky
wants to merge
16
commits into
konflux-ci:main
Choose a base branch
from
milantaky:analytics-config
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
3f75ab4
feat: adds segment package into project
milantaky c4ed432
feat: adds analytics types and defaults for runtime config
milantaky a12e3bc
feat: adds load-config and tests
milantaky 7f9cf3e
feat: intialization of analytics, calling it, handling failure
milantaky 2187b4a
feat: adds tests for initAnalytics
milantaky d4181ca
feat: corrects segment api parameter
milantaky b8152dd
fix: fixes tests
milantaky 510fd82
fix: cr suggestion
milantaky 89c980f
fix: adjusts inits in main.tsx
milantaky 8f6aa40
fix: removes duplicate failed init report
milantaky ebef258
fix: removes test for monitoring in analytics index
milantaky b4da4e5
fix: re-adds test for error handling
milantaky 65a6808
fix: sets default values to undefined, enhance inits
milantaky fe1989f
fix: updates tests
milantaky 9f1e5b6
fix: removes unnecesarry await
milantaky b09a2dd
Merge branch 'main' into analytics-config
milantaky File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| import { mockConsole, MockConsole } from '~/unit-test-utils'; | ||
| import type { AnalyticsConfig } from '../types'; | ||
|
|
||
| // Mock the Segment SDK | ||
| const mockAnalyticsInstance = { | ||
| track: jest.fn(), | ||
| identify: jest.fn(), | ||
| page: jest.fn(), | ||
| group: jest.fn(), | ||
| alias: jest.fn(), | ||
| }; | ||
|
|
||
| const mockAnalyticsBrowser = { | ||
| load: jest.fn().mockResolvedValue([mockAnalyticsInstance, {}]), | ||
| }; | ||
|
|
||
| jest.mock('@segment/analytics-next', () => ({ | ||
| AnalyticsBrowser: mockAnalyticsBrowser, | ||
| })); | ||
|
|
||
| jest.mock('../load-config', () => ({ | ||
| loadAnalyticsConfig: jest.fn(), | ||
| })); | ||
|
|
||
| describe('initAnalytics and getAnalytics', () => { | ||
| let consoleMock: MockConsole; | ||
| let loadAnalyticsConfigMock: jest.Mock; | ||
|
|
||
| beforeEach(() => { | ||
| consoleMock = mockConsole(); | ||
| jest.resetModules(); | ||
| loadAnalyticsConfigMock = jest.requireMock('../load-config').loadAnalyticsConfig; | ||
| mockAnalyticsBrowser.load.mockResolvedValue([mockAnalyticsInstance, {}]); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| consoleMock.restore(); | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe('initAnalytics', () => { | ||
| it('should successfully initialize with valid config', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: 'test-write-key-123', | ||
| apiUrl: 'https://api.segment.io/v1', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(loadAnalyticsConfigMock).toHaveBeenCalled(); | ||
| expect(mockAnalyticsBrowser.load).toHaveBeenCalledWith( | ||
| { writeKey: 'test-write-key-123' }, | ||
| { | ||
| integrations: { | ||
| 'Segment.io': { | ||
| apiHost: 'https://api.segment.io/v1', | ||
| protocol: 'https', | ||
| }, | ||
| }, | ||
| }, | ||
| ); | ||
| expect(indexModule.getAnalytics()).toBe(mockAnalyticsInstance); | ||
| expect(consoleMock.info).toHaveBeenCalledWith('Analytics loaded'); | ||
| }); | ||
|
|
||
| it('should not load SDK when analytics is disabled', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: false, | ||
| writeKey: 'test-write-key-123', | ||
| apiUrl: '', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(loadAnalyticsConfigMock).toHaveBeenCalled(); | ||
| expect(mockAnalyticsBrowser.load).not.toHaveBeenCalled(); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should not load SDK when apiUrl is missing', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: 'test-write-key-123', | ||
| apiUrl: '', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(mockAnalyticsBrowser.load).not.toHaveBeenCalled(); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should not load SDK when apiUrl is only whitespace', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: 'test-write-key-123', | ||
| apiUrl: ' ', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(mockAnalyticsBrowser.load).not.toHaveBeenCalled(); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should not load SDK when write key is missing', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: '', | ||
| apiUrl: '', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(mockAnalyticsBrowser.load).not.toHaveBeenCalled(); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should not load SDK when write key is only whitespace', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: ' ', | ||
| apiUrl: '', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(mockAnalyticsBrowser.load).not.toHaveBeenCalled(); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should trim write key and apiUrl before using', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: ' test-key ', | ||
| apiUrl: ' https://api.example.com ', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(mockAnalyticsBrowser.load).toHaveBeenCalledWith( | ||
| { writeKey: 'test-key' }, | ||
| { | ||
| integrations: { | ||
| 'Segment.io': { | ||
| apiHost: 'https://api.example.com', | ||
| protocol: 'https', | ||
| }, | ||
| }, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| it('should handle initialization errors gracefully', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: 'test-write-key-123', | ||
| apiUrl: 'https://api.segment.io/v1', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const initError = new Error('Failed to load Segment SDK'); | ||
| mockAnalyticsBrowser.load.mockRejectedValue(initError); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| await indexModule.initAnalytics(); | ||
|
|
||
| expect(consoleMock.error).toHaveBeenCalledWith('Error loading Analytics', initError); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
| }); | ||
| }); | ||
|
|
||
| describe('getAnalytics', () => { | ||
| it('should return undefined when analytics is not initialized', async () => { | ||
| const indexModule = await import('../index'); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('should return analytics instance after successful initialization', async () => { | ||
| const mockConfig: AnalyticsConfig = { | ||
| enabled: true, | ||
| writeKey: 'test-write-key-123', | ||
| apiUrl: 'https://api.segment.io/v1', | ||
| }; | ||
| loadAnalyticsConfigMock.mockReturnValue(mockConfig); | ||
|
|
||
| const indexModule = await import('../index'); | ||
| expect(indexModule.getAnalytics()).toBeUndefined(); | ||
|
|
||
| await indexModule.initAnalytics(); | ||
| expect(indexModule.getAnalytics()).toBe(mockAnalyticsInstance); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,81 @@ | ||
| import { loadAnalyticsConfig } from '../load-config'; | ||
|
|
||
| describe('loadAnalyticsConfig', () => { | ||
| const originalKonfluxRuntime = window.KONFLUX_RUNTIME; | ||
|
|
||
| afterEach(() => { | ||
| window.KONFLUX_RUNTIME = originalKonfluxRuntime; | ||
| }); | ||
|
|
||
| it('should return default config when window.KONFLUX_RUNTIME is undefined', () => { | ||
| window.KONFLUX_RUNTIME = undefined; | ||
|
|
||
| const config = loadAnalyticsConfig(); | ||
|
|
||
| expect(config).toEqual({ | ||
| enabled: false, | ||
| writeKey: undefined, | ||
| apiUrl: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it('should return default config when ANALYTICS_ENABLED is false', () => { | ||
| window.KONFLUX_RUNTIME = { | ||
| ANALYTICS_ENABLED: 'false', | ||
| ANALYTICS_WRITE_KEY: 'some-key', | ||
| ANALYTICS_API_URL: 'https://api.example.com', | ||
| }; | ||
|
|
||
| const config = loadAnalyticsConfig(); | ||
|
|
||
| expect(config).toEqual({ | ||
| enabled: false, | ||
| writeKey: undefined, | ||
| apiUrl: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it('should return default config when only ANALYTICS_ENABLED is set to false', () => { | ||
| window.KONFLUX_RUNTIME = { | ||
| ANALYTICS_ENABLED: 'false', | ||
| }; | ||
|
|
||
| const config = loadAnalyticsConfig(); | ||
|
|
||
| expect(config).toEqual({ | ||
| enabled: false, | ||
| writeKey: undefined, | ||
| apiUrl: undefined, | ||
| }); | ||
| }); | ||
|
|
||
| it('should return full config when analytics is enabled with all fields set', () => { | ||
| window.KONFLUX_RUNTIME = { | ||
| ANALYTICS_ENABLED: 'true', | ||
| ANALYTICS_WRITE_KEY: 'segment-write-key-123', | ||
| ANALYTICS_API_URL: 'https://api.segment.io/v1', | ||
| }; | ||
|
|
||
| const config = loadAnalyticsConfig(); | ||
|
|
||
| expect(config).toEqual({ | ||
| enabled: true, | ||
| writeKey: 'segment-write-key-123', | ||
| apiUrl: 'https://api.segment.io/v1', | ||
| }); | ||
| }); | ||
|
|
||
| it('should use empty string fallbacks when write key and api url are missing', () => { | ||
| window.KONFLUX_RUNTIME = { | ||
| ANALYTICS_ENABLED: 'true', | ||
| }; | ||
|
|
||
| const config = loadAnalyticsConfig(); | ||
|
|
||
| expect(config).toEqual({ | ||
| enabled: true, | ||
| writeKey: '', | ||
| apiUrl: '', | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import type { Analytics } from '@segment/analytics-next'; | ||
| import { loadAnalyticsConfig } from './load-config'; | ||
|
|
||
| let analyticsInstance: Analytics | undefined; | ||
|
|
||
| /** | ||
| * Returns the initialized Segment analytics instance, or undefined if analytics | ||
| * is disabled or not yet initialized. Callers must handle the undefined case. | ||
| */ | ||
| export function getAnalytics(): Analytics | undefined { | ||
| return analyticsInstance; | ||
| } | ||
|
|
||
| /** | ||
| * Initializes the Segment SDK when ANALYTICS_ENABLED is true and a valid write | ||
| * key is present. Uses dynamic import so the SDK is not in the main bundle when | ||
| * disabled. Errors are logged and reported to Sentry if available. | ||
| */ | ||
| export async function initAnalytics(): Promise<void> { | ||
| const config = loadAnalyticsConfig(); | ||
|
|
||
| const writeKey = config.writeKey?.trim(); | ||
| const apiHost = config.apiUrl?.trim(); | ||
| if (!config.enabled || !writeKey || !apiHost) { | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const { AnalyticsBrowser } = await import( | ||
| '@segment/analytics-next' /* webpackChunkName: "segment-analytics" */ | ||
| ); | ||
|
|
||
| const [analytics] = await AnalyticsBrowser.load( | ||
| { | ||
| writeKey, | ||
| }, | ||
| { | ||
| integrations: { | ||
| 'Segment.io': { | ||
| apiHost, | ||
| protocol: 'https', | ||
| }, | ||
| }, | ||
| }, | ||
| ); | ||
|
|
||
| analyticsInstance = analytics; | ||
| // eslint-disable-next-line no-console | ||
| console.info('Analytics loaded'); | ||
| } catch (error) { | ||
| // eslint-disable-next-line no-console | ||
| console.error('Error loading Analytics', error); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.