mirror of
https://github.com/caprover/caprover
synced 2025-10-30 01:57:03 +00:00
adding report for unused fields so that we can better prioritize adding support
This commit is contained in:
@@ -3,6 +3,7 @@ import axios from 'axios'
|
|||||||
import ApiStatusCodes from '../../../api/ApiStatusCodes'
|
import ApiStatusCodes from '../../../api/ApiStatusCodes'
|
||||||
import BaseApi from '../../../api/BaseApi'
|
import BaseApi from '../../../api/BaseApi'
|
||||||
import InjectionExtractor from '../../../injection/InjectionExtractor'
|
import InjectionExtractor from '../../../injection/InjectionExtractor'
|
||||||
|
import { EventLogger } from '../../../user/events/EventLogger'
|
||||||
import {
|
import {
|
||||||
CapRoverEventFactory,
|
CapRoverEventFactory,
|
||||||
CapRoverEventType,
|
CapRoverEventType,
|
||||||
@@ -276,9 +277,13 @@ router.post('/deploy', function (req, res, next) {
|
|||||||
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
InjectionExtractor.extractUserFromInjected(res).user.dataStore
|
||||||
const serviceManager =
|
const serviceManager =
|
||||||
InjectionExtractor.extractUserFromInjected(res).user.serviceManager
|
InjectionExtractor.extractUserFromInjected(res).user.serviceManager
|
||||||
|
const eventLogger =
|
||||||
|
InjectionExtractor.extractUserFromInjected(res).user.userManager
|
||||||
|
.eventLogger
|
||||||
|
|
||||||
const template = req.body.template
|
const template = req.body.template
|
||||||
const values = req.body.values
|
const values = req.body.values
|
||||||
|
const templateName = req.body.templateName
|
||||||
const deploymentJobRegistry = OneClickDeploymentJobRegistry.getInstance()
|
const deploymentJobRegistry = OneClickDeploymentJobRegistry.getInstance()
|
||||||
|
|
||||||
return Promise.resolve() //
|
return Promise.resolve() //
|
||||||
@@ -292,9 +297,7 @@ router.post('/deploy', function (req, res, next) {
|
|||||||
|
|
||||||
const jobId = deploymentJobRegistry.createJob()
|
const jobId = deploymentJobRegistry.createJob()
|
||||||
|
|
||||||
Logger.dev(`Starting one-click deployment with jobId: ${jobId}`)
|
reportAnalyticsOnAppDeploy(templateName, template, eventLogger)
|
||||||
Logger.dev(`Template: ${JSON.stringify(template, null, 2)}`)
|
|
||||||
Logger.dev(`Values: ${JSON.stringify(values, null, 2)}`)
|
|
||||||
|
|
||||||
new OneClickAppDeployManager(
|
new OneClickAppDeployManager(
|
||||||
dataStore,
|
dataStore,
|
||||||
@@ -366,3 +369,56 @@ router.get('/deploy/progress', function (req, res, next) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
||||||
|
// This function analyzes the provided template to identify any unused fields in Docker service definitions.
|
||||||
|
// It then logs an analytics event with the unused fields and the template name (if it's an official or known template).
|
||||||
|
// This helps track which fields users are using and may inform future improvements to the one-click app templates.
|
||||||
|
export function reportAnalyticsOnAppDeploy(
|
||||||
|
templateName: any,
|
||||||
|
template: any,
|
||||||
|
eventLogger: EventLogger
|
||||||
|
) {
|
||||||
|
const unusedDockerServiceFieldNames: string[] = []
|
||||||
|
if (
|
||||||
|
templateName === 'TEMPLATE_ONE_CLICK' ||
|
||||||
|
templateName === 'DOCKER_COMPOSE'
|
||||||
|
) {
|
||||||
|
if (template?.services) {
|
||||||
|
template.services.forEach((service: any) => {
|
||||||
|
if (service && typeof service === 'object') {
|
||||||
|
Object.keys(service).forEach((key) => {
|
||||||
|
if (
|
||||||
|
!'image,environment,ports,volumes,depends_on,hostname,command,cap_add'
|
||||||
|
.split(',')
|
||||||
|
.includes(key)
|
||||||
|
) {
|
||||||
|
// log the unused keys so that we can track what to add next
|
||||||
|
if (!unusedDockerServiceFieldNames.includes(key)) {
|
||||||
|
unusedDockerServiceFieldNames.push(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we do not want to log private repos names
|
||||||
|
const templateNameToReport =
|
||||||
|
templateName === 'TEMPLATE_ONE_CLICK' ||
|
||||||
|
templateName === 'DOCKER_COMPOSE' ||
|
||||||
|
(typeof templateName === 'string' &&
|
||||||
|
templateName.startsWith('OFFICIAL_'))
|
||||||
|
? templateName
|
||||||
|
: 'UNKNOWN'
|
||||||
|
|
||||||
|
eventLogger.trackEvent(
|
||||||
|
CapRoverEventFactory.create(
|
||||||
|
CapRoverEventType.OneClickAppDeployStarted,
|
||||||
|
{
|
||||||
|
unusedFields: unusedDockerServiceFieldNames,
|
||||||
|
templateName: templateNameToReport,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum CapRoverEventType {
|
|||||||
InstanceStarted = 'InstanceStarted',
|
InstanceStarted = 'InstanceStarted',
|
||||||
OneClickAppDetailsFetched = 'OneClickAppDetailsFetched',
|
OneClickAppDetailsFetched = 'OneClickAppDetailsFetched',
|
||||||
OneClickAppListFetched = 'OneClickAppListFetched',
|
OneClickAppListFetched = 'OneClickAppListFetched',
|
||||||
|
OneClickAppDeployStarted = 'OneClickAppDeployStarted',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICapRoverEvent {
|
export interface ICapRoverEvent {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class AnalyticsLogger extends IEventsEmitter {
|
|||||||
case CapRoverEventType.InstanceStarted:
|
case CapRoverEventType.InstanceStarted:
|
||||||
case CapRoverEventType.OneClickAppDetailsFetched:
|
case CapRoverEventType.OneClickAppDetailsFetched:
|
||||||
case CapRoverEventType.OneClickAppListFetched:
|
case CapRoverEventType.OneClickAppListFetched:
|
||||||
|
case CapRoverEventType.OneClickAppDeployStarted:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ export default class ProManager {
|
|||||||
case CapRoverEventType.InstanceStarted:
|
case CapRoverEventType.InstanceStarted:
|
||||||
case CapRoverEventType.OneClickAppDetailsFetched:
|
case CapRoverEventType.OneClickAppDetailsFetched:
|
||||||
case CapRoverEventType.OneClickAppListFetched:
|
case CapRoverEventType.OneClickAppListFetched:
|
||||||
|
case CapRoverEventType.OneClickAppDeployStarted:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
448
tests/OneClickAppRouter.test.ts
Normal file
448
tests/OneClickAppRouter.test.ts
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
/**
|
||||||
|
* TEST FILE: OneClickAppRouter.test.ts
|
||||||
|
*
|
||||||
|
* This file tests the `reportAnalyticsOnAppDeploy` function which tracks analytics
|
||||||
|
* when one-click apps or Docker Compose templates are deployed in CapRover.
|
||||||
|
*
|
||||||
|
* FUNCTION OVERVIEW: reportAnalyticsOnAppDeploy(templateName, template, eventLogger)
|
||||||
|
*
|
||||||
|
* PURPOSE:
|
||||||
|
* • Collects anonymous analytics about app deployments for usage insights
|
||||||
|
* • Tracks template usage patterns and custom Docker service fields
|
||||||
|
* • Helps identify which features are being used by the community
|
||||||
|
*
|
||||||
|
* TEMPLATE NAME HANDLING:
|
||||||
|
* • TEMPLATE_ONE_CLICK - One-click app templates (reported as-is)
|
||||||
|
* • DOCKER_COMPOSE - User-provided Docker Compose files (reported as-is)
|
||||||
|
* • OFFICIAL_* - Official CapRover templates (reported as-is)
|
||||||
|
* • Custom/private names - Anonymized as "UNKNOWN" for privacy
|
||||||
|
* • Invalid inputs (null, undefined, non-string) - Defaults to "UNKNOWN"
|
||||||
|
*
|
||||||
|
* DOCKER SERVICE FIELD TRACKING:
|
||||||
|
* • Only tracks unused fields for TEMPLATE_ONE_CLICK and DOCKER_COMPOSE
|
||||||
|
* • Known Docker fields (image, ports, volumes, environment, etc.) are consumed by CapRover
|
||||||
|
* • Custom/unknown fields are collected in an array for analytics
|
||||||
|
* • Helps identify which Docker features users need but aren't supported
|
||||||
|
* • Handles edge cases: null services, non-object entries, missing properties
|
||||||
|
*
|
||||||
|
* EVENT LOGGING:
|
||||||
|
* • Creates a CapRoverEventType.OneClickAppDetailsFetched event
|
||||||
|
* • Event metadata includes: templateName (string) and unusedFields (string[])
|
||||||
|
* • Uses CapRoverEventFactory to create events with proper structure
|
||||||
|
* • Calls eventLogger.trackEvent() exactly once per deployment
|
||||||
|
*
|
||||||
|
* ERROR HANDLING:
|
||||||
|
* • Robust handling of malformed inputs (null, undefined, wrong types)
|
||||||
|
* • Graceful degradation - never throws errors, always logs something
|
||||||
|
* • Array deduplication for repeated custom field names across services
|
||||||
|
* • Safe iteration over potentially corrupted service arrays
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Jest hoists this mock before the imports
|
||||||
|
jest.mock('../src/user/events/ICapRoverEvent', () => ({
|
||||||
|
CapRoverEventFactory: {
|
||||||
|
create: jest.fn().mockImplementation((type: any, data: any) => ({
|
||||||
|
eventType: type,
|
||||||
|
eventMetadata: data,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
CapRoverEventType: {
|
||||||
|
OneClickAppDetailsFetched: 'OneClickAppDetailsFetched',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
import { reportAnalyticsOnAppDeploy } from '../src/routes/user/oneclick/OneClickAppRouter'
|
||||||
|
import { EventLogger } from '../src/user/events/EventLogger'
|
||||||
|
import {
|
||||||
|
CapRoverEventFactory,
|
||||||
|
CapRoverEventType,
|
||||||
|
ICapRoverEvent,
|
||||||
|
} from '../src/user/events/ICapRoverEvent'
|
||||||
|
|
||||||
|
describe('reportAnalyticsOnAppDeploy', () => {
|
||||||
|
let mockEventLogger: EventLogger
|
||||||
|
let trackedEvents: ICapRoverEvent[]
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
trackedEvents = []
|
||||||
|
|
||||||
|
// Create a proper mock that satisfies EventLogger interface
|
||||||
|
mockEventLogger = {
|
||||||
|
trackEvent: jest.fn((event: ICapRoverEvent) => {
|
||||||
|
trackedEvents.push(event)
|
||||||
|
}),
|
||||||
|
} as any as EventLogger
|
||||||
|
|
||||||
|
// Reset the factory mock
|
||||||
|
;(CapRoverEventFactory.create as jest.Mock).mockClear()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Template name handling', () => {
|
||||||
|
test('should report TEMPLATE_ONE_CLICK template name as-is', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe(
|
||||||
|
'TEMPLATE_ONE_CLICK'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should report DOCKER_COMPOSE template name as-is', () => {
|
||||||
|
const templateName = 'DOCKER_COMPOSE'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe(
|
||||||
|
'DOCKER_COMPOSE'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should report OFFICIAL_ prefixed template names as-is', () => {
|
||||||
|
const templateName = 'OFFICIAL_NGINX'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe(
|
||||||
|
'OFFICIAL_NGINX'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should report private/custom template names as UNKNOWN', () => {
|
||||||
|
const templateName = 'my-private-template'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe('UNKNOWN')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle null templateName', () => {
|
||||||
|
const templateName = null
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe('UNKNOWN')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle undefined templateName', () => {
|
||||||
|
const templateName = undefined
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe('UNKNOWN')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Docker service field tracking', () => {
|
||||||
|
test('should not track unused fields for non-Docker templates', () => {
|
||||||
|
const templateName = 'SOME_OTHER_TEMPLATE'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'nginx:latest',
|
||||||
|
custom_field: 'value',
|
||||||
|
another_field: 'value2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(trackedEvents[0].eventMetadata.unusedFields).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should track unused fields for TEMPLATE_ONE_CLICK', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'nginx:latest',
|
||||||
|
environment: { VAR: 'value' },
|
||||||
|
custom_field: 'value',
|
||||||
|
another_field: 'value2',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual(['custom_field', 'another_field'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should track unused fields for DOCKER_COMPOSE', () => {
|
||||||
|
const templateName = 'DOCKER_COMPOSE'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'postgres:latest',
|
||||||
|
volumes: ['/data:/var/lib/postgresql/data'],
|
||||||
|
restart: 'always',
|
||||||
|
networks: ['backend'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual(['restart', 'networks'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should not track known Docker service fields', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'nginx:latest',
|
||||||
|
environment: { VAR: 'value' },
|
||||||
|
ports: ['80:80'],
|
||||||
|
volumes: ['/data:/data'],
|
||||||
|
depends_on: ['db'],
|
||||||
|
hostname: 'web',
|
||||||
|
command: 'nginx -g "daemon off;"',
|
||||||
|
cap_add: ['SYS_ADMIN'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle multiple services with mixed fields', () => {
|
||||||
|
const templateName = 'DOCKER_COMPOSE'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'nginx:latest',
|
||||||
|
ports: ['80:80'],
|
||||||
|
custom_field1: 'value1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'postgres:latest',
|
||||||
|
environment: { POSTGRES_DB: 'mydb' },
|
||||||
|
custom_field2: 'value2',
|
||||||
|
another_custom: 'value3',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual([
|
||||||
|
'custom_field1',
|
||||||
|
'custom_field2',
|
||||||
|
'another_custom',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle services with no custom fields', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'nginx:latest',
|
||||||
|
ports: ['80:80'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle empty services array', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle template without services property', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = { other_property: 'value' }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle null template', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = null
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle undefined template', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = undefined
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Event logging', () => {
|
||||||
|
test('should call eventLogger.trackEvent exactly once', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should create event with correct type', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(trackedEvents[0].eventType).toBe(
|
||||||
|
CapRoverEventType.OneClickAppDetailsFetched
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should create event with correct metadata structure', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'nginx:latest',
|
||||||
|
custom_field: 'value',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const eventMetadata = trackedEvents[0].eventMetadata
|
||||||
|
expect(eventMetadata).toHaveProperty('unusedFields')
|
||||||
|
expect(eventMetadata).toHaveProperty('templateName')
|
||||||
|
expect(Array.isArray(eventMetadata.unusedFields)).toBe(true)
|
||||||
|
expect(typeof eventMetadata.templateName).toBe('string')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Edge cases and error handling', () => {
|
||||||
|
test('should handle services with null/undefined service objects', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = {
|
||||||
|
services: [null, undefined, { image: 'nginx:latest' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
reportAnalyticsOnAppDeploy(
|
||||||
|
templateName,
|
||||||
|
template,
|
||||||
|
mockEventLogger
|
||||||
|
)
|
||||||
|
}).not.toThrow()
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle services array containing non-object values', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = {
|
||||||
|
services: ['string', 123, true, { image: 'nginx:latest' }],
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
reportAnalyticsOnAppDeploy(
|
||||||
|
templateName,
|
||||||
|
template,
|
||||||
|
mockEventLogger
|
||||||
|
)
|
||||||
|
}).not.toThrow()
|
||||||
|
|
||||||
|
expect(mockEventLogger.trackEvent).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle template name that is not a string', () => {
|
||||||
|
const templateName = 123
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe('UNKNOWN')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should handle empty string template name', () => {
|
||||||
|
const templateName = ''
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(trackedEvents[0].eventMetadata.templateName).toBe('UNKNOWN')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should preserve array uniqueness for duplicate unused fields', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = {
|
||||||
|
services: [
|
||||||
|
{
|
||||||
|
image: 'nginx:latest',
|
||||||
|
custom_field: 'value1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
image: 'postgres:latest',
|
||||||
|
custom_field: 'value2', // Same field name, different value
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
const unusedFields = trackedEvents[0].eventMetadata.unusedFields
|
||||||
|
expect(unusedFields).toEqual(['custom_field'])
|
||||||
|
expect(unusedFields.length).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Integration with CapRoverEventFactory', () => {
|
||||||
|
test('should use CapRoverEventFactory.create method', () => {
|
||||||
|
const templateName = 'TEMPLATE_ONE_CLICK'
|
||||||
|
const template = { services: [] }
|
||||||
|
|
||||||
|
reportAnalyticsOnAppDeploy(templateName, template, mockEventLogger)
|
||||||
|
|
||||||
|
expect(CapRoverEventFactory.create).toHaveBeenCalledWith(
|
||||||
|
CapRoverEventType.OneClickAppDetailsFetched,
|
||||||
|
{
|
||||||
|
unusedFields: [],
|
||||||
|
templateName: 'TEMPLATE_ONE_CLICK',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user