feat(audience): ✨ Add corporate affiliation filter to AudienceQueryDto and update AudienceService for corp-based audience queries
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
4245e76119
commit
2b362cde74
2 changed files with 84 additions and 22 deletions
|
|
@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { AudienceQueryDto, GeoQueryDto, DeviceQueryDto, GeoGranularity } from './dto/audience-query.dto';
|
||||
import { resolveCorpId, corpSessionFilter } from '../common/corp-filter.util';
|
||||
|
||||
export interface AudienceOverview {
|
||||
totalUsers: number;
|
||||
|
|
@ -88,6 +89,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getOverview(query: AudienceQueryDto): Promise<AudienceOverview> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(3, corpId);
|
||||
|
||||
const overviewQuery = `
|
||||
WITH user_sessions AS (
|
||||
|
|
@ -99,7 +102,7 @@ export class AudienceService {
|
|||
sf."createdAt"
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${corpClause}
|
||||
),
|
||||
user_first AS (
|
||||
SELECT "userId", MIN("createdAt") as first_session
|
||||
|
|
@ -116,8 +119,10 @@ export class AudienceService {
|
|||
LEFT JOIN user_first uf ON us."userId" = uf."userId"
|
||||
`;
|
||||
|
||||
const overviewParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
|
||||
|
||||
const [overview, topCountries, topDevices] = await Promise.all([
|
||||
this.dataSource.query(overviewQuery, [startDate, endDate]),
|
||||
this.dataSource.query(overviewQuery, overviewParams),
|
||||
this.getGeography({ ...query, granularity: GeoGranularity.COUNTRY, limit: 5 }),
|
||||
this.getDevices({ ...query, limit: 5 }),
|
||||
]);
|
||||
|
|
@ -143,6 +148,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getDemographics(query: AudienceQueryDto): Promise<DemographicsMetrics[]> {
|
||||
const { startDate, endDate } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(3, corpId);
|
||||
|
||||
const demographicsQuery = `
|
||||
WITH user_sessions AS (
|
||||
|
|
@ -171,7 +178,7 @@ export class AudienceService {
|
|||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."userId" IS NOT NULL
|
||||
AND sf."userId" IS NOT NULL${corpClause}
|
||||
),
|
||||
user_first AS (
|
||||
SELECT "userId", MIN("createdAt") as first_session
|
||||
|
|
@ -198,7 +205,8 @@ export class AudienceService {
|
|||
`;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(demographicsQuery, [startDate, endDate]);
|
||||
const demoParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
|
||||
const result = await this.dataSource.query(demographicsQuery, demoParams);
|
||||
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
userType: row.user_type as 'new' | 'returning',
|
||||
|
|
@ -278,6 +286,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getBrowsers(query: AudienceQueryDto): Promise<BrowserMetrics[]> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(4, corpId);
|
||||
|
||||
const browsersQuery = `
|
||||
WITH session_data AS (
|
||||
|
|
@ -288,7 +298,7 @@ export class AudienceService {
|
|||
COALESCE(sf."browserVersion", '') as version
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${corpClause}
|
||||
),
|
||||
total AS (
|
||||
SELECT COUNT(*) as total_sessions FROM session_data
|
||||
|
|
@ -307,7 +317,10 @@ export class AudienceService {
|
|||
`;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(browsersQuery, [startDate, endDate, limit ?? 20]);
|
||||
const browserParams: unknown[] = corpId === null
|
||||
? [startDate, endDate, limit ?? 20]
|
||||
: [startDate, endDate, limit ?? 20, corpId];
|
||||
const result = await this.dataSource.query(browsersQuery, browserParams);
|
||||
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
browser: row.browser as string,
|
||||
|
|
@ -327,6 +340,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getOperatingSystems(query: AudienceQueryDto): Promise<OSMetrics[]> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(4, corpId);
|
||||
|
||||
const osQuery = `
|
||||
WITH session_data AS (
|
||||
|
|
@ -337,7 +352,7 @@ export class AudienceService {
|
|||
COALESCE(sf."osVersion", '') as version
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${corpClause}
|
||||
),
|
||||
total AS (
|
||||
SELECT COUNT(*) as total_sessions FROM session_data
|
||||
|
|
@ -356,7 +371,10 @@ export class AudienceService {
|
|||
`;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(osQuery, [startDate, endDate, limit ?? 20]);
|
||||
const osParams: unknown[] = corpId === null
|
||||
? [startDate, endDate, limit ?? 20]
|
||||
: [startDate, endDate, limit ?? 20, corpId];
|
||||
const result = await this.dataSource.query(osQuery, osParams);
|
||||
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
os: row.os as string,
|
||||
|
|
@ -395,6 +413,13 @@ export class AudienceService {
|
|||
paramIndex++;
|
||||
}
|
||||
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
if (corpId !== null) {
|
||||
conditions.push(`sf."sessionId" IN (SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $${paramIndex})`);
|
||||
params.push(corpId);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const whereClause = conditions.join(' AND ');
|
||||
|
||||
const locationColumn = this.getGeoColumn(granularity);
|
||||
|
|
@ -462,6 +487,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getLanguages(query: AudienceQueryDto): Promise<LanguageMetrics[]> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(4, corpId);
|
||||
|
||||
const languagesQuery = `
|
||||
WITH session_data AS (
|
||||
|
|
@ -471,7 +498,7 @@ export class AudienceService {
|
|||
COALESCE(sf.language, 'unknown') as language
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${corpClause}
|
||||
),
|
||||
total AS (
|
||||
SELECT COUNT(*) as total_sessions FROM session_data
|
||||
|
|
@ -489,7 +516,10 @@ export class AudienceService {
|
|||
`;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(languagesQuery, [startDate, endDate, limit ?? 20]);
|
||||
const langParams: unknown[] = corpId === null
|
||||
? [startDate, endDate, limit ?? 20]
|
||||
: [startDate, endDate, limit ?? 20, corpId];
|
||||
const result = await this.dataSource.query(languagesQuery, langParams);
|
||||
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
language: row.language as string,
|
||||
|
|
@ -508,6 +538,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getNewVsReturning(query: AudienceQueryDto): Promise<NewVsReturningMetrics[]> {
|
||||
const { startDate, endDate } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(3, corpId);
|
||||
|
||||
const trendQuery = `
|
||||
WITH daily_sessions AS (
|
||||
|
|
@ -517,7 +549,7 @@ export class AudienceService {
|
|||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."userId" IS NOT NULL
|
||||
AND sf."userId" IS NOT NULL${corpClause}
|
||||
),
|
||||
user_first AS (
|
||||
SELECT "userId", MIN("createdAt") as first_session
|
||||
|
|
@ -536,7 +568,8 @@ export class AudienceService {
|
|||
`;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(trendQuery, [startDate, endDate]);
|
||||
const trendParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
|
||||
const result = await this.dataSource.query(trendQuery, trendParams);
|
||||
|
||||
return result.map((row: Record<string, unknown>) => {
|
||||
const newUsers = Number(row.new_users) || 0;
|
||||
|
|
@ -562,6 +595,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getNetworkType(query: AudienceQueryDto): Promise<Array<{ orgType: string; sessions: number; percentage: number }>> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(4, corpId);
|
||||
|
||||
const sql = `
|
||||
WITH session_data AS (
|
||||
|
|
@ -570,7 +605,7 @@ export class AudienceService {
|
|||
COALESCE(sf."orgType", 'NORMAL') as org_type
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${corpClause}
|
||||
),
|
||||
total AS (SELECT COUNT(*) as total_sessions FROM session_data)
|
||||
SELECT
|
||||
|
|
@ -596,7 +631,10 @@ export class AudienceService {
|
|||
};
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(sql, [startDate, endDate, limit ?? 20]);
|
||||
const ntParams: unknown[] = corpId === null
|
||||
? [startDate, endDate, limit ?? 20]
|
||||
: [startDate, endDate, limit ?? 20, corpId];
|
||||
const result = await this.dataSource.query(sql, ntParams);
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
orgType: ORG_TYPE_LABELS[String(row.org_type)] ?? String(row.org_type),
|
||||
sessions: Number(row.sessions) || 0,
|
||||
|
|
@ -613,6 +651,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getProxyType(query: AudienceQueryDto): Promise<Array<{ proxyType: string; sessions: number; percentage: number }>> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(4, corpId);
|
||||
|
||||
const sql = `
|
||||
WITH session_data AS (
|
||||
|
|
@ -621,7 +661,7 @@ export class AudienceService {
|
|||
COALESCE(sf."proxyType", 'NONE') as proxy_type
|
||||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf."isBot" = false${corpClause}
|
||||
),
|
||||
total AS (SELECT COUNT(*) as total_sessions FROM session_data)
|
||||
SELECT
|
||||
|
|
@ -646,7 +686,10 @@ export class AudienceService {
|
|||
};
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(sql, [startDate, endDate, limit ?? 20]);
|
||||
const ptParams: unknown[] = corpId === null
|
||||
? [startDate, endDate, limit ?? 20]
|
||||
: [startDate, endDate, limit ?? 20, corpId];
|
||||
const result = await this.dataSource.query(sql, ptParams);
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
proxyType: PROXY_TYPE_LABELS[String(row.proxy_type)] ?? String(row.proxy_type),
|
||||
sessions: Number(row.sessions) || 0,
|
||||
|
|
@ -663,6 +706,8 @@ export class AudienceService {
|
|||
*/
|
||||
async getOrganizations(query: AudienceQueryDto): Promise<Array<{ org: string; sessions: number; percentage: number }>> {
|
||||
const { startDate, endDate, limit } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpSessionFilter(4, corpId);
|
||||
|
||||
const sql = `
|
||||
WITH session_data AS (
|
||||
|
|
@ -672,7 +717,7 @@ export class AudienceService {
|
|||
FROM session_fingerprints sf
|
||||
WHERE sf."createdAt" BETWEEN $1 AND $2
|
||||
AND sf."isBot" = false
|
||||
AND sf.org IS NOT NULL
|
||||
AND sf.org IS NOT NULL${corpClause}
|
||||
),
|
||||
total AS (SELECT COUNT(*) as total_sessions FROM session_data)
|
||||
SELECT
|
||||
|
|
@ -687,7 +732,10 @@ export class AudienceService {
|
|||
`;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(sql, [startDate, endDate, limit ?? 20]);
|
||||
const orgParams: unknown[] = corpId === null
|
||||
? [startDate, endDate, limit ?? 20]
|
||||
: [startDate, endDate, limit ?? 20, corpId];
|
||||
const result = await this.dataSource.query(sql, orgParams);
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
org: String(row.org),
|
||||
sessions: Number(row.sessions) || 0,
|
||||
|
|
@ -708,6 +756,10 @@ export class AudienceService {
|
|||
breakdown: Array<{ orgType: string; responseTier: string; count: number }>;
|
||||
}> {
|
||||
const { startDate, endDate } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpId === null
|
||||
? ''
|
||||
: ` AND "sessionId" IN (SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $3)`;
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
|
|
@ -717,7 +769,7 @@ export class AudienceService {
|
|||
FROM session_fingerprints
|
||||
WHERE "createdAt" BETWEEN $1 AND $2
|
||||
AND "isGovernment" = true
|
||||
AND "isBot" = false
|
||||
AND "isBot" = false${corpClause}
|
||||
GROUP BY "orgType", "responseTier"
|
||||
ORDER BY count DESC
|
||||
`;
|
||||
|
|
@ -733,7 +785,8 @@ export class AudienceService {
|
|||
};
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(sql, [startDate, endDate]);
|
||||
const govParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
|
||||
const result = await this.dataSource.query(sql, govParams);
|
||||
const breakdown = result.map((row: Record<string, unknown>) => ({
|
||||
orgType: ORG_TYPE_LABELS[String(row.org_type)] ?? String(row.org_type),
|
||||
responseTier: String(row.response_tier),
|
||||
|
|
@ -766,6 +819,10 @@ export class AudienceService {
|
|||
lastSeen: string;
|
||||
}>> {
|
||||
const { startDate, endDate } = query;
|
||||
const corpId = await resolveCorpId(this.dataSource, query.corp);
|
||||
const corpClause = corpId === null
|
||||
? ''
|
||||
: ` AND "sessionId" IN (SELECT DISTINCT "sessionId" FROM raw_events WHERE corp_id = $3)`;
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
|
|
@ -786,14 +843,15 @@ export class AudienceService {
|
|||
OR "asn" IS NOT NULL
|
||||
OR "orgType" NOT IN ('NORMAL', 'UNKNOWN')
|
||||
OR "proxyType" NOT IN ('NONE', 'UNKNOWN')
|
||||
)
|
||||
)${corpClause}
|
||||
GROUP BY "org", "asn", "orgType", "proxyType", "responseTier", "country"
|
||||
ORDER BY sessions DESC
|
||||
LIMIT 100
|
||||
`;
|
||||
|
||||
try {
|
||||
const result = await this.dataSource.query(sql, [startDate, endDate]);
|
||||
const ipParams: unknown[] = corpId === null ? [startDate, endDate] : [startDate, endDate, corpId];
|
||||
const result = await this.dataSource.query(sql, ipParams);
|
||||
return result.map((row: Record<string, unknown>) => ({
|
||||
org: row.org ? String(row.org) : null,
|
||||
asn: row.asn ? Number(row.asn) : null,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ export class AudienceQueryDto {
|
|||
@Min(1)
|
||||
@Max(100)
|
||||
limit?: number = 20;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
corp?: string;
|
||||
}
|
||||
|
||||
export class GeoQueryDto extends AudienceQueryDto {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue