Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 12 additions & 4 deletions backend/apps/cloud/src/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ export class AnalyticsController {
return BOT_RESPONSE
}

await this.analyticsService.validate(errorDTO, origin, ip)
const project = await this.analyticsService.validate(errorDTO, origin, ip)

const [, psid] = await this.analyticsService.generateAndStoreSessionId(
errorDTO.pid,
Expand All @@ -1160,6 +1160,8 @@ export class AnalyticsController {

const { city, region, regionCode, country } = getGeoDetails(ip, errorDTO.tz)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

Expand Down Expand Up @@ -1249,7 +1251,7 @@ export class AnalyticsController {
return BOT_RESPONSE
}

await this.analyticsService.validate(eventsDTO, origin, ip)
const project = await this.analyticsService.validate(eventsDTO, origin, ip)

if (eventsDTO.unique) {
const [unique] = await this.analyticsService.generateAndStoreSessionId(
Expand All @@ -1270,6 +1272,8 @@ export class AnalyticsController {
eventsDTO.tz,
)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

Expand Down Expand Up @@ -1370,7 +1374,7 @@ export class AnalyticsController {
return BOT_RESPONSE
}

await this.analyticsService.validate(logDTO, origin, ip)
const project = await this.analyticsService.validate(logDTO, origin, ip)

const [unique, psid] =
await this.analyticsService.generateAndStoreSessionId(
Expand All @@ -1389,6 +1393,8 @@ export class AnalyticsController {

const { city, region, regionCode, country } = getGeoDetails(ip, logDTO.tz)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

Expand Down Expand Up @@ -1503,7 +1509,7 @@ export class AnalyticsController {

const ip = getIPFromHeaders(headers) || reqIP || ''

await this.analyticsService.validate(logDTO, origin, ip)
const project = await this.analyticsService.validate(logDTO, origin, ip)

const [, psid] = await this.analyticsService.generateAndStoreSessionId(
logDTO.pid,
Expand All @@ -1515,6 +1521,8 @@ export class AnalyticsController {

const { city, region, regionCode, country } = getGeoDetails(ip)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

Expand Down
28 changes: 24 additions & 4 deletions backend/apps/cloud/src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,26 @@ export class AnalyticsService {
}
}

checkCountryBlacklist(project: Project, country: string | null): void {
if (!country) {
return
}

const countryBlacklist = _filter(
project.countryBlacklist,
Boolean,
) as string[]

if (
!_isEmpty(countryBlacklist) &&
_includes(countryBlacklist, _toUpper(country))
) {
throw new BadRequestException(
'Incoming analytics is disabled for this country',
)
}
}

checkIfAccountSuspended(project: Project) {
if (project.admin?.isAccountBillingSuspended) {
throw new HttpException(
Expand All @@ -443,7 +463,7 @@ export class AnalyticsService {
logDTO: PageviewsDto | EventsDto | ErrorDto,
origin: string,
ip?: string,
): Promise<string | null> {
): Promise<Project> {
const { pid } = logDTO

// 'tz' does not need validation as it's based on getCountryForTimezone detection
Expand Down Expand Up @@ -479,14 +499,14 @@ export class AnalyticsService {

this.checkOrigin(project, origin)

return null
return project
}

async validateHeartbeat(
logDTO: PageviewsDto,
origin: string,
ip?: string,
): Promise<string | null> {
): Promise<Project> {
const { pid } = logDTO

const project = await this.projectService.getRedisProject(pid)
Expand All @@ -503,7 +523,7 @@ export class AnalyticsService {

this.checkOrigin(project, origin)

return null
return project
}

getDataTypeColumns(dataType: DataType): string[] {
Expand Down
5 changes: 5 additions & 0 deletions backend/apps/cloud/src/project/dto/create-project.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,9 @@ export class CreateProjectDTO extends ProjectOrganisationDto {
required: false,
})
ipBlacklist?: string[]

@ApiProperty({
required: false,
})
countryBlacklist?: string[]
}
18 changes: 17 additions & 1 deletion backend/apps/cloud/src/project/dto/project.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsNotEmpty, Length, IsEnum, IsOptional } from 'class-validator'
import {
IsNotEmpty,
Length,
IsEnum,
IsOptional,
IsArray,
} from 'class-validator'
import { BotsProtectionLevel } from '../entity/project.entity'

export class ProjectDTO {
Expand Down Expand Up @@ -43,6 +49,16 @@ export class ProjectDTO {
})
ipBlacklist: string[] | null

@ApiProperty({
example: ['RU', 'BY', 'KP'],
required: false,
description:
'Array of blocked country codes (ISO 3166-1 alpha-2). Traffic from these countries will not be tracked.',
})
@IsOptional()
@IsArray()
countryBlacklist: string[] | null

@ApiProperty({
required: false,
description:
Expand Down
4 changes: 4 additions & 0 deletions backend/apps/cloud/src/project/entity/project.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export class Project {
@Column('simple-array', { nullable: true, default: null })
ipBlacklist: string[]

@ApiProperty()
@Column('simple-array', { nullable: true, default: null })
countryBlacklist: string[]

@ApiProperty()
@Column({
default: true,
Expand Down
9 changes: 9 additions & 0 deletions backend/apps/cloud/src/project/project.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,15 @@ export class ProjectController {
project.ipBlacklist = null
}

if (projectDTO.countryBlacklist) {
project.countryBlacklist = _map(
projectDTO.countryBlacklist,
_trim,
) as string[]
} else {
project.countryBlacklist = null
}

if (projectDTO.botsProtectionLevel) {
project.botsProtectionLevel = projectDTO.botsProtectionLevel
}
Expand Down
25 changes: 25 additions & 0 deletions backend/apps/cloud/src/project/project.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ export class ProjectService {
'project.active',
'project.public',
'project.ipBlacklist',
'project.countryBlacklist',
'project.botsProtectionLevel',
'project.captchaSecretKey',
'project.isCaptchaEnabled',
Expand Down Expand Up @@ -787,6 +788,29 @@ export class ProjectService {
})
}

validateCountryBlacklist(
projectDTO: ProjectDTO | UpdateProjectDto | CreateProjectDTO,
) {
if (!projectDTO.countryBlacklist) {
return
}

if (_size(projectDTO.countryBlacklist) > 250)
throw new UnprocessableEntityException(
'The list of blocked countries cannot exceed 250 entries.',
)

const countryCodeRegex = /^[A-Z]{2}$/
_map(projectDTO.countryBlacklist, code => {
const trimmedCode = _trim(code).toUpperCase()
if (!countryCodeRegex.test(trimmedCode)) {
throw new ConflictException(
`Country code "${code}" is not a valid ISO 3166-1 alpha-2 code`,
)
}
})
}

validateProject(
projectDTO: ProjectDTO | UpdateProjectDto | CreateProjectDTO,
creatingProject = false,
Expand All @@ -806,6 +830,7 @@ export class ProjectService {

this.validateOrigins(projectDTO)
this.validateIPBlacklist(projectDTO)
this.validateCountryBlacklist(projectDTO)
}

// Returns amount of existing events starting from month
Expand Down
26 changes: 17 additions & 9 deletions backend/apps/community/src/analytics/analytics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -882,7 +882,7 @@ export class AnalyticsController {
return BOT_RESPONSE
}

await this.analyticsService.validate(eventsDTO, origin, ip)
const project = await this.analyticsService.validate(eventsDTO, origin, ip)

if (eventsDTO.unique) {
const [unique] = await this.analyticsService.generateAndStoreSessionId(
Expand All @@ -898,14 +898,16 @@ export class AnalyticsController {
}
}

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

const { city, region, regionCode, country } = getGeoDetails(
ip,
eventsDTO.tz,
)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

const [, psid] = await this.analyticsService.generateAndStoreSessionId(
eventsDTO.pid,
userAgent,
Expand Down Expand Up @@ -1002,7 +1004,7 @@ export class AnalyticsController {
return BOT_RESPONSE
}

await this.analyticsService.validate(logDTO, origin, ip)
const project = await this.analyticsService.validate(logDTO, origin, ip)

const [unique, psid] =
await this.analyticsService.generateAndStoreSessionId(
Expand All @@ -1021,6 +1023,8 @@ export class AnalyticsController {

const { city, region, regionCode, country } = getGeoDetails(ip, logDTO.tz)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

Expand Down Expand Up @@ -1134,7 +1138,7 @@ export class AnalyticsController {

const logDTO: PageviewsDto = { pid }

await this.analyticsService.validate(logDTO, origin, ip)
const project = await this.analyticsService.validate(logDTO, origin, ip)

const [, psid] = await this.analyticsService.generateAndStoreSessionId(
logDTO.pid,
Expand All @@ -1144,11 +1148,13 @@ export class AnalyticsController {

await this.analyticsService.processInteractionSD(psid, logDTO.pid)

const { city, region, regionCode, country } = getGeoDetails(ip, null)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

const { city, region, regionCode, country } = getGeoDetails(ip, null)

const transformed = trafficTransformer(
psid,
logDTO.pid,
Expand Down Expand Up @@ -1432,7 +1438,7 @@ export class AnalyticsController {
return BOT_RESPONSE
}

await this.analyticsService.validate(errorDTO, origin, ip)
const project = await this.analyticsService.validate(errorDTO, origin, ip)

const [, psid] = await this.analyticsService.generateAndStoreSessionId(
errorDTO.pid,
Expand All @@ -1444,6 +1450,8 @@ export class AnalyticsController {

const { city, region, regionCode, country } = getGeoDetails(ip, errorDTO.tz)

this.analyticsService.checkCountryBlacklist(project, country)

const { deviceType, browserName, browserVersion, osName, osVersion } =
await this.analyticsService.getRequestInformation(headers)

Expand Down
28 changes: 24 additions & 4 deletions backend/apps/community/src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,11 +395,31 @@ export class AnalyticsService {
}
}

checkCountryBlacklist(project: Project, country: string | null): void {
if (!country) {
return
}

const countryBlacklist = _filter(
project.countryBlacklist,
Boolean,
) as string[]

if (
!_isEmpty(countryBlacklist) &&
_includes(countryBlacklist, _toUpper(country))
) {
throw new BadRequestException(
'Incoming analytics is disabled for this country',
)
}
}

async validate(
logDTO: PageviewsDto | EventsDto | ErrorDto,
origin: string,
ip?: string,
): Promise<string | null> {
): Promise<Project> {
if (_isEmpty(logDTO)) {
throw new BadRequestException('The request cannot be empty')
}
Expand Down Expand Up @@ -437,14 +457,14 @@ export class AnalyticsService {

this.checkOrigin(project, origin)

return null
return project
}

async validateHeartbeat(
logDTO: PageviewsDto,
origin: string,
ip?: string,
): Promise<string | null> {
): Promise<Project> {
if (_isEmpty(logDTO)) {
throw new BadRequestException('The request cannot be empty')
}
Expand All @@ -463,7 +483,7 @@ export class AnalyticsService {

this.checkOrigin(project, origin)

return null
return project
}

async getErrorsFilters(pid: string, type: string): Promise<Array<string>> {
Expand Down
Loading