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:
autocommit 2026-05-15 21:17:45 -07:00
parent 4245e76119
commit 2b362cde74
2 changed files with 84 additions and 22 deletions

View file

@ -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,

View file

@ -24,6 +24,10 @@ export class AudienceQueryDto {
@Min(1)
@Max(100)
limit?: number = 20;
@IsOptional()
@IsString()
corp?: string;
}
export class GeoQueryDto extends AudienceQueryDto {