feat(telemetry): 设备字段提升为顶层结构化列(Amplitude 风格)
将 device_brand/device_model/device_os/app_version/locale 从 JSONB properties 提升为 analytics_event_log 表的独立列,并建立索引,支持亿级数据量下的高效 按设备维度查询和分组统计。 前端 (mining-app + mobile-app): - toServerJson() 从 properties 中提取设备字段,以顶层字段发送给服务端 - 本地存储格式不变(properties 仍保留设备字段,便于离线队列完整性) 后端 (presence-service): - Prisma schema: EventLog 新增 deviceBrand/deviceModel/deviceOs/appVersion/locale 列 - Migration: ALTER TABLE 添加 5 列 + 2 个索引 - DTO/Command: EventItemDto 接收顶层设备字段 - Entity: EventLog 新增 5 个字段及 getter - Mapper: toDomain/toPersistence 映射新字段 - Handler: toEventLog 从 DTO 读取设备字段;SessionStartedEvent 优先使用顶层字段 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
482df12f91
commit
6bca65e434
|
|
@ -0,0 +1,11 @@
|
|||
-- AlterTable: add device context columns to analytics_event_log
|
||||
ALTER TABLE "analytics_event_log"
|
||||
ADD COLUMN "device_brand" VARCHAR(64),
|
||||
ADD COLUMN "device_model" VARCHAR(64),
|
||||
ADD COLUMN "device_os" VARCHAR(32),
|
||||
ADD COLUMN "app_version" VARCHAR(32),
|
||||
ADD COLUMN "locale" VARCHAR(16);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "idx_event_log_device_brand" ON "analytics_event_log"("device_brand");
|
||||
CREATE INDEX "idx_event_log_app_version" ON "analytics_event_log"("app_version");
|
||||
|
|
@ -18,6 +18,11 @@ model EventLog {
|
|||
installId String @map("install_id") @db.VarChar(64)
|
||||
eventName String @map("event_name") @db.VarChar(64)
|
||||
eventTime DateTime @map("event_time") @db.Timestamptz()
|
||||
deviceBrand String? @map("device_brand") @db.VarChar(64)
|
||||
deviceModel String? @map("device_model") @db.VarChar(64)
|
||||
deviceOs String? @map("device_os") @db.VarChar(32)
|
||||
appVersion String? @map("app_version") @db.VarChar(32)
|
||||
locale String? @map("locale") @db.VarChar(16)
|
||||
properties Json? @db.JsonB
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz()
|
||||
|
||||
|
|
@ -25,6 +30,8 @@ model EventLog {
|
|||
@@index([eventName], name: "idx_event_log_event_name")
|
||||
@@index([eventName, eventTime], name: "idx_event_log_event_name_time")
|
||||
@@index([userId], name: "idx_event_log_user_id")
|
||||
@@index([deviceBrand], name: "idx_event_log_device_brand")
|
||||
@@index([appVersion], name: "idx_event_log_app_version")
|
||||
@@map("analytics_event_log")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,32 @@ export class EventItemDto {
|
|||
@IsNumber()
|
||||
clientTs: number;
|
||||
|
||||
@ApiPropertyOptional({ description: '事件属性' })
|
||||
@ApiPropertyOptional({ description: '设备品牌', example: 'Xiaomi' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deviceBrand?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '设备型号', example: 'Redmi Note 12' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deviceModel?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '操作系统版本', example: '13' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deviceOs?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'App版本号', example: '1.2.0' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
appVersion?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '语言/地区', example: 'zh_CN' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
locale?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: '事件专属属性' })
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
properties?: Record<string, unknown>;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,11 @@ export interface EventItemDto {
|
|||
userId?: string;
|
||||
installId: string;
|
||||
clientTs: number;
|
||||
deviceBrand?: string;
|
||||
deviceModel?: string;
|
||||
deviceOs?: string;
|
||||
appVersion?: string;
|
||||
locale?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,9 +72,9 @@ export class RecordEventsHandler implements ICommandHandler<RecordEventsCommand>
|
|||
log.installId.value,
|
||||
log.eventTime,
|
||||
{
|
||||
appVersion: log.properties.appVersion,
|
||||
os: log.properties.os,
|
||||
osVersion: log.properties.osVersion,
|
||||
appVersion: log.appVersion ?? log.properties.appVersion,
|
||||
os: log.deviceOs ?? log.properties.os,
|
||||
osVersion: log.deviceOs ?? log.properties.osVersion,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
|
@ -101,6 +101,11 @@ export class RecordEventsHandler implements ICommandHandler<RecordEventsCommand>
|
|||
installId: InstallId.fromString(dto.installId),
|
||||
eventName: EventName.fromString(dto.eventName),
|
||||
eventTime: new Date(dto.clientTs * 1000),
|
||||
deviceBrand: dto.deviceBrand ?? null,
|
||||
deviceModel: dto.deviceModel ?? null,
|
||||
deviceOs: dto.deviceOs ?? null,
|
||||
appVersion: dto.appVersion ?? null,
|
||||
locale: dto.locale ?? null,
|
||||
properties: EventProperties.fromData(dto.properties ?? {}),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,11 @@ export class EventLog {
|
|||
private _installId: InstallId;
|
||||
private _eventName: EventName;
|
||||
private _eventTime: Date;
|
||||
private _deviceBrand: string | null;
|
||||
private _deviceModel: string | null;
|
||||
private _deviceOs: string | null;
|
||||
private _appVersion: string | null;
|
||||
private _locale: string | null;
|
||||
private _properties: EventProperties;
|
||||
private _createdAt: Date;
|
||||
|
||||
|
|
@ -34,6 +39,26 @@ export class EventLog {
|
|||
return this._eventTime;
|
||||
}
|
||||
|
||||
get deviceBrand(): string | null {
|
||||
return this._deviceBrand;
|
||||
}
|
||||
|
||||
get deviceModel(): string | null {
|
||||
return this._deviceModel;
|
||||
}
|
||||
|
||||
get deviceOs(): string | null {
|
||||
return this._deviceOs;
|
||||
}
|
||||
|
||||
get appVersion(): string | null {
|
||||
return this._appVersion;
|
||||
}
|
||||
|
||||
get locale(): string | null {
|
||||
return this._locale;
|
||||
}
|
||||
|
||||
get properties(): EventProperties {
|
||||
return this._properties;
|
||||
}
|
||||
|
|
@ -56,6 +81,11 @@ export class EventLog {
|
|||
installId: InstallId;
|
||||
eventName: EventName;
|
||||
eventTime: Date;
|
||||
deviceBrand?: string | null;
|
||||
deviceModel?: string | null;
|
||||
deviceOs?: string | null;
|
||||
appVersion?: string | null;
|
||||
locale?: string | null;
|
||||
properties?: EventProperties;
|
||||
}): EventLog {
|
||||
const log = new EventLog();
|
||||
|
|
@ -64,6 +94,11 @@ export class EventLog {
|
|||
log._installId = props.installId;
|
||||
log._eventName = props.eventName;
|
||||
log._eventTime = props.eventTime;
|
||||
log._deviceBrand = props.deviceBrand ?? null;
|
||||
log._deviceModel = props.deviceModel ?? null;
|
||||
log._deviceOs = props.deviceOs ?? null;
|
||||
log._appVersion = props.appVersion ?? null;
|
||||
log._locale = props.locale ?? null;
|
||||
log._properties = props.properties ?? EventProperties.empty();
|
||||
log._createdAt = new Date();
|
||||
return log;
|
||||
|
|
@ -76,6 +111,11 @@ export class EventLog {
|
|||
installId: InstallId;
|
||||
eventName: EventName;
|
||||
eventTime: Date;
|
||||
deviceBrand: string | null;
|
||||
deviceModel: string | null;
|
||||
deviceOs: string | null;
|
||||
appVersion: string | null;
|
||||
locale: string | null;
|
||||
properties: EventProperties;
|
||||
createdAt: Date;
|
||||
}): EventLog {
|
||||
|
|
@ -85,6 +125,11 @@ export class EventLog {
|
|||
log._installId = props.installId;
|
||||
log._eventName = props.eventName;
|
||||
log._eventTime = props.eventTime;
|
||||
log._deviceBrand = props.deviceBrand;
|
||||
log._deviceModel = props.deviceModel;
|
||||
log._deviceOs = props.deviceOs;
|
||||
log._appVersion = props.appVersion;
|
||||
log._locale = props.locale;
|
||||
log._properties = props.properties;
|
||||
log._createdAt = props.createdAt;
|
||||
return log;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ export class EventLogMapper {
|
|||
installId: InstallId.fromString(prisma.installId),
|
||||
eventName: EventName.fromString(prisma.eventName),
|
||||
eventTime: prisma.eventTime,
|
||||
deviceBrand: prisma.deviceBrand,
|
||||
deviceModel: prisma.deviceModel,
|
||||
deviceOs: prisma.deviceOs,
|
||||
appVersion: prisma.appVersion,
|
||||
locale: prisma.locale,
|
||||
properties: EventProperties.fromData((prisma.properties as EventPropertiesData) ?? {}),
|
||||
createdAt: prisma.createdAt,
|
||||
});
|
||||
|
|
@ -27,6 +32,11 @@ export class EventLogMapper {
|
|||
installId: domain.installId.value,
|
||||
eventName: domain.eventName.value,
|
||||
eventTime: domain.eventTime,
|
||||
deviceBrand: domain.deviceBrand,
|
||||
deviceModel: domain.deviceModel,
|
||||
deviceOs: domain.deviceOs,
|
||||
appVersion: domain.appVersion,
|
||||
locale: domain.locale,
|
||||
properties: domain.properties.toJSON() as Prisma.InputJsonValue,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -118,15 +118,31 @@ class TelemetryEvent extends Equatable {
|
|||
};
|
||||
}
|
||||
|
||||
/// 转换为服务端 API 格式
|
||||
/// 转换为服务端 API 格式(Amplitude 风格:设备字段为顶层独立列,方便服务端索引)
|
||||
/// 顶层字段: eventName, userId, installId, clientTs, deviceBrand, deviceModel,
|
||||
/// deviceOs, appVersion, locale
|
||||
/// properties: 仅保留事件专属数据(页面名、金额等)
|
||||
Map<String, dynamic> toServerJson() {
|
||||
// 从 properties 中提取设备字段(提升为顶层),剩余为事件专属数据
|
||||
final props = Map<String, dynamic>.from(properties ?? {});
|
||||
final deviceBrand = props.remove('device_brand');
|
||||
final deviceModel = props.remove('device_model');
|
||||
final deviceOs = props.remove('device_os');
|
||||
final appVersion = props.remove('app_version');
|
||||
final locale = props.remove('locale');
|
||||
|
||||
return {
|
||||
'eventName': name,
|
||||
'userId': userId,
|
||||
'installId': installId,
|
||||
'clientTs': timestamp.millisecondsSinceEpoch ~/ 1000,
|
||||
'deviceBrand': deviceBrand,
|
||||
'deviceModel': deviceModel,
|
||||
'deviceOs': deviceOs,
|
||||
'appVersion': appVersion,
|
||||
'locale': locale,
|
||||
'properties': {
|
||||
...?properties,
|
||||
...props,
|
||||
'eventId': eventId,
|
||||
'type': type.name,
|
||||
'level': level.name,
|
||||
|
|
|
|||
|
|
@ -118,21 +118,31 @@ class TelemetryEvent extends Equatable {
|
|||
};
|
||||
}
|
||||
|
||||
/// 转换为服务端 API 格式
|
||||
/// 后端 presence-service 期望的格式:
|
||||
/// - eventName: string (required)
|
||||
/// - userId: string (optional) - userSerialNum, e.g. "D25121400005"
|
||||
/// - installId: string (required)
|
||||
/// - clientTs: number (required) - Unix timestamp in seconds
|
||||
/// - properties: object (optional)
|
||||
/// 转换为服务端 API 格式(Amplitude 风格:设备字段为顶层独立列,方便服务端索引)
|
||||
/// 顶层字段: eventName, userId, installId, clientTs, deviceBrand, deviceModel,
|
||||
/// deviceOs, appVersion, locale
|
||||
/// properties: 仅保留事件专属数据(页面名、金额等)
|
||||
Map<String, dynamic> toServerJson() {
|
||||
// 从 properties 中提取设备字段(提升为顶层),剩余为事件专属数据
|
||||
final props = Map<String, dynamic>.from(properties ?? {});
|
||||
final deviceBrand = props.remove('device_brand');
|
||||
final deviceModel = props.remove('device_model');
|
||||
final deviceOs = props.remove('device_os');
|
||||
final appVersion = props.remove('app_version');
|
||||
final locale = props.remove('locale');
|
||||
|
||||
return {
|
||||
'eventName': name,
|
||||
'userId': userId,
|
||||
'installId': installId,
|
||||
'clientTs': timestamp.millisecondsSinceEpoch ~/ 1000,
|
||||
'deviceBrand': deviceBrand,
|
||||
'deviceModel': deviceModel,
|
||||
'deviceOs': deviceOs,
|
||||
'appVersion': appVersion,
|
||||
'locale': locale,
|
||||
'properties': {
|
||||
...?properties,
|
||||
...props,
|
||||
'eventId': eventId,
|
||||
'type': type.name,
|
||||
'level': level.name,
|
||||
|
|
|
|||
Loading…
Reference in New Issue