Skip to content
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

(feature) Numeral custom events metadata / e-commerce #238

Merged
merged 25 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5c92cd7
Add Views Entity and update Project Entity
pro1code1hack Jun 19, 2024
61d9ec7
Create cascade for retrieving e-commerce views
pro1code1hack Jun 19, 2024
4b88787
Restructure the repo according to the task
pro1code1hack Jun 20, 2024
e6a410e
Add support for roles protection
pro1code1hack Jun 20, 2024
106998c
Add name column to ProjectViews
pro1code1hack Jun 20, 2024
975b572
Create project views endpoint
pro1code1hack Jun 24, 2024
45735fb
Add Enum type to the View DTO and Entity
pro1code1hack Jun 24, 2024
bdc4f39
Add ProjectViewEntity, validation for /log endpoint
pro1code1hack Jun 25, 2024
3b36361
Update ProjectViewCustomEventDto with CreateProjectViewDTO
pro1code1hack Jun 25, 2024
8519c47
Add endpoint to update views
pro1code1hack Jun 26, 2024
4a356a6
Add endpoint to remove views
pro1code1hack Jun 26, 2024
94964f8
Add customevents fir views
pro1code1hack Jun 26, 2024
fd52855
Complete e-commerce feature tracking, add clickhouse query for analyt…
pro1code1hack Jul 1, 2024
5c5b433
Swagger endpoint update
pro1code1hack Jul 1, 2024
785d8bb
Remove logging
pro1code1hack Jul 1, 2024
6b7a9b8
Add validationw with RegEx for custom events
pro1code1hack Jul 8, 2024
31941d9
Add view for a single project
pro1code1hack Jul 8, 2024
945fa3a
Add migration
pro1code1hack Jul 8, 2024
4b18ef9
fix linters
pro1code1hack Jul 9, 2024
145b6e5
- Refactor and simplify query for custom events
pro1code1hack Jul 12, 2024
41ffc73
update findProject
pro1code1hack Jul 12, 2024
156d934
Remove log line
pro1code1hack Jul 12, 2024
e8fa51f
fix linters
pro1code1hack Jul 12, 2024
faf903d
Fix validation and update SQL queries
pro1code1hack Jul 15, 2024
2ca3c5d
Fix SQL injection for query
pro1code1hack Jul 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion apps/production/src/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import {
ForbiddenException,
Response,
Header,
ConflictException,
NotFoundException,
} from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'
import * as UAParser from 'ua-parser-js'
import * as isbot from 'isbot'

Expand Down Expand Up @@ -90,6 +92,7 @@ import { GetErrorsDto } from './dto/get-errors.dto'
import { GetErrorDTO } from './dto/get-error.dto'
import { PatchStatusDTO } from './dto/patch-status.dto'
import { ProjectService } from '../project/project.service'
import { ProjectsViewsRepository } from '../project/repositories/projects-views.repository'

// eslint-disable-next-line @typescript-eslint/no-var-requires
const mysql = require('mysql2')
Expand Down Expand Up @@ -368,8 +371,10 @@ export class AnalyticsController {
private readonly analyticsService: AnalyticsService,
private readonly logger: AppLoggerService,
private readonly projectService: ProjectService,
private readonly projectsViewsRepository: ProjectsViewsRepository,
) {}

@ApiBearerAuth()
@Get()
@Auth([], true, true)
async getData(
Expand All @@ -387,6 +392,7 @@ export class AnalyticsController {
filters,
timezone = DEFAULT_TIMEZONE,
mode = ChartRenderMode.PERIODICAL,
viewId,
} = data
this.analyticsService.validatePID(pid)

Expand All @@ -402,6 +408,30 @@ export class AnalyticsController {

await this.analyticsService.checkBillingAccess(pid)

if (viewId && filters) {
throw new ConflictException('Cannot specify both viewId and filters.')
}

const view = await this.projectsViewsRepository.findProjectView(pid, viewId)

if (!view) {
throw new NotFoundException('View not found.')
}

const customEvents = view.customEvents.map(event => ({
customEventName: event.customEventName,
metaKey: event.metaKey,
metaValue: event.metaValue,
metaValueType: event.metaValueType,
}))

const metaKeys = customEvents.map(event => event.metaKey)
const metaResult = await this.analyticsService.getMetaResult(
pid,
metaKeys,
customEvents,
)

let newTimebucket = timeBucket
let allowedTumebucketForPeriodAll

Expand Down Expand Up @@ -513,6 +543,7 @@ export class AnalyticsController {
properties,
appliedFilters,
timeBucket: allowedTumebucketForPeriodAll,
meta: metaResult,
}
}

Expand Down
72 changes: 72 additions & 0 deletions apps/production/src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
} from './interfaces'
import { ErrorDTO } from './dto/error.dto'
import { GetPagePropertyMetaDTO } from './dto/get-page-property-meta.dto'
import { ProjectViewCustomEventMetaValueType } from '../project/entity/project-view-custom-event.entity'

dayjs.extend(utc)
dayjs.extend(dayjsTimezone)
Expand Down Expand Up @@ -3670,4 +3671,75 @@
events,
}
}

async getMetaResult(
pid: string,
metaKeys: string[],
customEvents: {
customEventName: string
metaKey: string
metaValue: string
metaValueType: string
}[],
) {
const params = {
pid,
..._reduce(
metaKeys,
(acc, key, index) => ({ ...acc, [`metaKey_${index}`]: key }),
{},
),
}

const casesSum = customEvents
.map(
(event, index) => `
WHEN key = {metaKey_${index}:String} THEN ${
event.metaValueType === ProjectViewCustomEventMetaValueType.INTEGER
? 'toInt32OrZero(value)'
: 'toFloat32OrZero(value)'
}
`,
)
.join(' ')

const casesAvg = customEvents
.map(
(event, index) => `
WHEN key = {metaKey_${index}:String} THEN ${
event.metaValueType === ProjectViewCustomEventMetaValueType.INTEGER
? 'toInt32OrZero(value)'
: 'toFloat32OrZero(value)'
}
`,
)
.join(' ')

const metaKeysParams = metaKeys
.map((_, index) => `{metaKey_${index}:String}`)
.join(', ')

const query = `
SELECT
key,
sum(CASE ${casesSum} ELSE 0 END) AS sum,
avg(CASE ${casesAvg} ELSE 0 END) AS avg
FROM customEV
ARRAY JOIN meta.key AS key, meta.value AS value
WHERE pid = {pid:FixedString(12)} AND key IN (${metaKeysParams})
GROUP BY key
`

try {
const result = await clickhouse.query(query, { params }).toPromise()
return result
} catch (reason) {
console.error('[ERROR] (getMetaResult) - Clickhouse query error:')
console.error(reason)
throw new InternalServerErrorException(
'Error occurred while fetching meta results',
)
}
}

Check failure on line 3744 in apps/production/src/analytics/analytics.service.ts

View workflow job for this annotation

GitHub Actions / checks (16.19.x)

Delete `⏎`
}
13 changes: 10 additions & 3 deletions apps/production/src/analytics/dto/getData.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsNotEmpty } from 'class-validator'
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'
import { DEFAULT_TIMEZONE } from '../../user/entities/user.entity'

export enum TimeBucketType {
Expand Down Expand Up @@ -28,15 +28,16 @@ export class AnalyticsGET_DTO {
@IsNotEmpty()
timeBucket: TimeBucketType

@ApiProperty()
@ApiProperty({ required: false })
from: string

@ApiProperty()
@ApiProperty({ required: false })
to: string

@ApiProperty({
description:
'A stringified array of properties to filter [{ column, filter, isExclusive }]',
required: false,
})
filters: string

Expand All @@ -52,4 +53,10 @@ export class AnalyticsGET_DTO {
default: ChartRenderMode.PERIODICAL,
})
mode?: ChartRenderMode

@ApiProperty({ description: 'The id of the view' })
@IsUUID('4')
@IsString()
@IsOptional()
viewId: string
}
187 changes: 187 additions & 0 deletions apps/production/src/project/dto/create-project-view.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import {
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
Matches,
MaxLength,
MinLength,
IsLocale,
ValidateNested,
registerDecorator,
ValidationOptions,
ValidatorConstraint,
ValidatorConstraintInterface,
isISO31661Alpha2,
IsUrl,
} from 'class-validator'

import { ApiProperty } from '@nestjs/swagger'
import { ProjectViewType } from '../entity/project-view.entity'
import { ProjectViewCustomEventMetaValueType } from '../entity/project-view-custom-event.entity'

// This can be updated by the customer request.
// In case we find out that ``cc`` may have any other values, just extend the ``allowedValues`` defined below
// Current implementation is for ``Cloudflare``

const allowedValues = ['T1', 'XX']

@ValidatorConstraint({ async: false })
export class IsISO31661Alpha2OrCustomConstraint
implements ValidatorConstraintInterface
{
validate(value: any) {
return isISO31661Alpha2(value) || allowedValues.includes(value)
}

defaultMessage() {
return `cc must be a valid ISO 3166-1 alpha-2 country code or one of the following: ${allowedValues}`
}
}

export function IsISO31661Alpha2OrCustom(
validationOptions?: ValidationOptions,
) {
return function closure(object: object, propertyName: string) {
// changed from Object to object
registerDecorator({
target: object.constructor,
propertyName,
options: validationOptions,
constraints: [],
validator: IsISO31661Alpha2OrCustomConstraint,
})
}
}

export class ProjectViewCustomEventDto {
@ApiProperty()
@MaxLength(100)
@MinLength(1)
@IsString()
@IsNotEmpty()
customEventName: string

@ApiProperty()
@MaxLength(100)
@MinLength(1)
@IsString()
@IsNotEmpty()
metaKey: string

@ApiProperty()
@MaxLength(100)
@MinLength(1)
@IsString()
@IsNotEmpty()
metaValue: string

@ApiProperty({ enum: ProjectViewCustomEventMetaValueType })
@IsEnum(ProjectViewCustomEventMetaValueType)
@IsNotEmpty()
metaValueType: ProjectViewCustomEventMetaValueType
}

export class CreateProjectViewDto {
@ApiProperty()
@MaxLength(100)
@MinLength(1)
@IsString()
@IsNotEmpty()
name: string

@ApiProperty({ description: 'Type of the view', enum: ProjectViewType })
@IsEnum(ProjectViewType)
@IsNotEmpty()
type: ProjectViewType

@ApiProperty({ description: 'Page the user viewed (/hello)', nullable: true })
/* eslint-disable-next-line */
@Matches(/^\/[a-zA-Z0-9-_\/\[\]]*$/, {
message: 'Invalid URL path format for pg',
})
@IsOptional()
pg?: string

@ApiProperty({ description: 'Name of the custom event (e.g., sign_up)' })
@MaxLength(100)
@MinLength(1)
@IsString()
@IsOptional()
ev?: string

@ApiProperty({
description: 'User device (mobile, desktop, tablet, etc.)',
nullable: true,
})
@IsString()
@IsOptional()
dv?: string

@ApiProperty({ description: 'Browser', nullable: true })
@IsString()
@IsOptional()
br?: string

@ApiProperty({ description: 'Operating system', nullable: true })
@IsString()
@IsOptional()
os?: string

@ApiProperty({ description: 'Locale (en-UK, en-US)', nullable: true })
@IsLocale()
@IsOptional()
lc?: string

@ApiProperty({
description:
'Referrer (site from which the user came to the site using Swetrix)',
nullable: true,
})
@IsUrl()
@IsOptional()
ref?: string

@ApiProperty({ description: 'UTM source', nullable: true })
@MaxLength(100)
@MinLength(1)
@IsString()
@IsOptional()
so?: string

@ApiProperty({ description: 'UTM medium', nullable: true })
@MaxLength(100)
@MinLength(1)
@IsString()
@IsOptional()
me?: string

@ApiProperty({ description: 'UTM campaign', nullable: true })
@MaxLength(100)
@MinLength(1)
@IsString()
@IsOptional()
ca?: string

@ApiProperty({ description: 'Country code', nullable: true })
@IsISO31661Alpha2OrCustom()
@IsNotEmpty()
cc: string

@ApiProperty({ description: 'Region/state (Alabama, etc.)', nullable: true })
@MaxLength(100)
@MinLength(1)
@IsString()
rg?: string

@ApiProperty({ description: 'City (Berlin, London, etc.)', nullable: true })
@MaxLength(100)
@MinLength(1)
@IsString()
ct?: string

@ApiProperty({ type: ProjectViewCustomEventDto, isArray: true })
@ValidateNested()
@IsNotEmpty()
customEvents: ProjectViewCustomEventDto[]
}
11 changes: 11 additions & 0 deletions apps/production/src/project/dto/project-id.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'
import { Matches, IsNotEmpty } from 'class-validator'

const PROJECT_ID_REGEX = /^(?!.*--)[a-zA-Z0-9-]{12}$/

export class ProjectIdDto {
@ApiProperty()
@Matches(PROJECT_ID_REGEX)
@IsNotEmpty()
readonly projectId: string
}
Loading
Loading