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: Experiment Archive State #987

Merged
merged 23 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
34b6e10
working archive feature frontend changes
ppratikcr7 Aug 14, 2023
38caaeb
ignoring storing logs for archived experiments
ppratikcr7 Aug 21, 2023
b056ac9
add few more missing readonly properties for archive state
ppratikcr7 Aug 24, 2023
e2954e1
missed readonly for archive state
ppratikcr7 Aug 24, 2023
62ea73c
Create API and database for archive
Aug 26, 2023
bd8b669
resolved failing test cases for archive state
ppratikcr7 Aug 28, 2023
5259dff
Merge branch 'dev' of https://github.com/CarnegieLearningWeb/UpGrade …
ppratikcr7 Aug 28, 2023
f72c420
import change
ppratikcr7 Aug 28, 2023
d0a7763
not allowing archived state change from enrolling state
ppratikcr7 Aug 31, 2023
fba94cf
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Aug 31, 2023
e8b9913
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 1, 2023
3be74d6
merge analysis and archieve api
Sep 11, 2023
06fcd6c
added remaining file
Sep 11, 2023
8bce729
fix peer review comments for archive state
ppratikcr7 Sep 12, 2023
6ac950d
Merge branch 'dev' into feature/archive-status-backend-issue30
ppratikcr7 Sep 12, 2023
b4d1b23
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 14, 2023
5aaf70b
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 19, 2023
57e3198
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Sep 20, 2023
d1a2d82
Solve review comments
Sep 22, 2023
03bfe20
Merge branch 'dev' into feature/archive-status-backend-issue30
ppratikcr7 Sep 29, 2023
be8bb4f
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Oct 16, 2023
8e40d34
Merge branch 'dev' into feature/archive-status-backend-issue30
ppratikcr7 Oct 26, 2023
f13630a
Merge branch 'dev' into feature/archive-status-backend-issue30
danoswaltCL Oct 26, 2023
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
36 changes: 36 additions & 0 deletions backend/packages/Upgrade/src/api/controllers/QueryController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,40 @@ export class QueryController {
): Promise<any> {
return this.queryService.analyze(dataLogParams.queryIds, request.logger);
}

ppratikcr7 marked this conversation as resolved.
Show resolved Hide resolved
/**
* @swagger
* /query/archive:
* post:
* description: Data log analysis
* consumes:
* - application/json
* parameters:
* - in: body
* name: params
* required: true
* schema:
* type: object
* properties:
* queryIds:
* type: array
* items:
* type: string
* description: Data analysis
* tags:
* - Query
* produces:
* - application/json
* responses:
* '200':
* description: Get data analysis
*/
@Post('/archive')
public archive(
@Body()
dataLogParams: DataLogAnalysisValidator,
@Req() request: AppRequest
): Promise<any> {
return this.queryService.getArchivedStats(dataLogParams.queryIds, request.logger);
}
}
18 changes: 18 additions & 0 deletions backend/packages/Upgrade/src/api/models/ArchivedStats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Entity, PrimaryColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { IsNotEmpty } from 'class-validator';
import { BaseModel } from './base/BaseModel';
import { Query } from './Query';

@Entity()
export class ArchivedStats extends BaseModel {
@PrimaryColumn('uuid')
public id: string;

@IsNotEmpty()
@Column('jsonb')
public result: any;
ppratikcr7 marked this conversation as resolved.
Show resolved Hide resolved

@OneToOne(() => Query, (query) => query.archivedStats, { onDelete: 'CASCADE' })
@JoinColumn()
public query: Query;
}
8 changes: 7 additions & 1 deletion backend/packages/Upgrade/src/api/models/Query.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Entity, ManyToOne, Column, PrimaryColumn } from 'typeorm';
import { Entity, ManyToOne, Column, PrimaryColumn, OneToOne } from 'typeorm';
import { BaseModel } from './base/BaseModel';
import { Metric } from './Metric';
import { Experiment } from './Experiment';
import { IsDefined } from 'class-validator';
import { REPEATED_MEASURE } from 'upgrade_types';
import { ArchivedStats } from './ArchivedStats';
import { Type } from 'class-transformer';

@Entity()
export class Query extends BaseModel {
Expand All @@ -23,6 +25,10 @@ export class Query extends BaseModel {
@ManyToOne(() => Experiment, (experiment) => experiment.queries, { onDelete: 'CASCADE' })
public experiment: Experiment;

@OneToOne(() => ArchivedStats, (archivedStats) => archivedStats.query, { onDelete: 'CASCADE' })
@Type(() => ArchivedStats)
public archivedStats: ArchivedStats;

@Column({
type: 'enum',
enum: REPEATED_MEASURE,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { EntityRepository, Repository } from 'typeorm';
import { ArchivedStats } from '../models/ArchivedStats';
import repositoryError from './utils/repositoryError';

@EntityRepository(ArchivedStats)
export class ArchivedStatsRepository extends Repository<ArchivedStats> {
public async saveRawJson(
rawDataArray: Array<Omit<ArchivedStats, 'createdAt' | 'updatedAt' | 'versionNumber'>>
): Promise<ArchivedStats> {
const result = await this.createQueryBuilder('ArchivedStats')
.insert()
.into(ArchivedStats)
.values(rawDataArray)
.onConflict(`DO NOTHING`)
.returning('*')
.execute()
.catch((errorMsg: any) => {
const errorMsgString = repositoryError(this.constructor.name, 'saveRawJson', { rawDataArray }, errorMsg);
throw errorMsgString;
});

return result.raw;
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Metric } from '../models/Metric';
import { EntityRepository, Repository } from 'typeorm';
import repositoryError from './utils/repositoryError';
import { EXPERIMENT_STATE } from 'upgrade_types';

@EntityRepository(Metric)
export class MetricRepository extends Repository<Metric> {
Expand Down Expand Up @@ -32,7 +33,9 @@ export class MetricRepository extends Repository<Metric> {
public async findMetricsWithQueries(ids: string[]): Promise<Metric[]> {
VivekFitkariwala marked this conversation as resolved.
Show resolved Hide resolved
return this.createQueryBuilder('metrics')
.innerJoin('metrics.queries', 'queries')
.innerJoin('queries.experiment', 'experiment')
.where('key IN (:...ids)', { ids })
.andWhere('experiment.state NOT IN (:...archived)', { archived: [EXPERIMENT_STATE.ARCHIVED] })
.getMany()
.catch((errorMsg: any) => {
const errorMsgString = repositoryError(this.constructor.name, 'findMetricsWithQueries', { ids }, errorMsg);
Expand Down
27 changes: 25 additions & 2 deletions backend/packages/Upgrade/src/api/services/ExperimentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ import {
import { ConditionPayloadDTO } from '../DTO/ConditionPayloadDTO';
import { FactorDTO } from '../DTO/FactorDTO';
import { LevelDTO } from '../DTO/LevelDTO';
import { QueryService } from './QueryService';
import { ArchivedStats } from '../models/ArchivedStats';
import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository';

@Service()
export class ExperimentService {
Expand All @@ -93,10 +96,12 @@ export class ExperimentService {
@OrmRepository() private factorRepository: FactorRepository,
@OrmRepository() private levelRepository: LevelRepository,
@OrmRepository() private levelCombinationElementsRepository: LevelCombinationElementRepository,
@OrmRepository() private archivedStatsRepository: ArchivedStatsRepository,
public previewUserService: PreviewUserService,
public segmentService: SegmentService,
public scheduledJobService: ScheduledJobService,
public errorService: ErrorService
public errorService: ErrorService,
public queryService: QueryService
) {}

public async find(logger?: UpgradeLogger): Promise<ExperimentDTO[]> {
Expand Down Expand Up @@ -384,7 +389,7 @@ export class ExperimentService {
): Promise<Experiment> {
const oldExperiment = await this.experimentRepository.findOne(
{ id: experimentId },
{ relations: ['stateTimeLogs'] }
{ relations: ['stateTimeLogs', 'queries', 'queries.metric'] }
);

if (
Expand All @@ -394,6 +399,24 @@ export class ExperimentService {
await this.populateExclusionTable(experimentId, state, logger);
}

if (state === EXPERIMENT_STATE.ARCHIVED) {
const queryIds = oldExperiment.queries.map((query) => query.id);
const results = await this.queryService.analyze(queryIds, logger);
const archivedStatsData: Array<Omit<ArchivedStats, 'createdAt' | 'updatedAt' | 'versionNumber'>> = results.map(
(result) => {
const queryId = result.id;
delete result.id;
const archivedStats: Partial<ArchivedStats> = {
id: uuid(),
result: result,
query: queryId,
};
return archivedStats;
}
);
await this.archivedStatsRepository.saveRawJson(archivedStatsData);
}

let data: AuditLogData = {
experimentId,
experimentName: oldExperiment.name,
Expand Down
14 changes: 14 additions & 0 deletions backend/packages/Upgrade/src/api/services/QueryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { ErrorService } from './ErrorService';
import { ExperimentError } from '../models/ExperimentError';
import { UpgradeLogger } from '../../lib/logger/UpgradeLogger';
import { Experiment } from '../models/Experiment';
import { ArchivedStatsRepository } from '../repositories/ArchivedStatsRepository';
import { In } from 'typeorm';

interface queryResult {
conditionId?: string;
Expand All @@ -21,6 +23,7 @@ export class QueryService {
constructor(
@OrmRepository() private queryRepository: QueryRepository,
@OrmRepository() private logRepository: LogRepository,
@OrmRepository() private archivedStatsRepository: ArchivedStatsRepository,
public errorService: ErrorService
) {}

Expand All @@ -35,6 +38,17 @@ export class QueryService {
});
}

public async getArchivedStats(queryIds: string[], logger: UpgradeLogger): Promise<any> {
logger.info({ message: `Get archivedStats of query with queryIds ${queryIds}` });
const archiveData = await this.archivedStatsRepository.find({
relations: ['query'],
where: { query: In(queryIds) },
});
return archiveData.map((data) => {
return { ...data.result, id: data.query.id };
});
}

public async analyze(queryIds: string[], logger: UpgradeLogger): Promise<any> {
logger.info({ message: `Get analysis of query with queryIds ${queryIds}` });
const promiseArray = queryIds.map((queryId) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class archivedState1692936809279 implements MigrationInterface {
name = 'archivedState1692936809279';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "archived_stats" ("createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), "versionNumber" integer NOT NULL, "id" uuid NOT NULL, "result" jsonb NOT NULL, "queryId" uuid, CONSTRAINT "REL_df18fe5ea7298a1dc7bfb33b06" UNIQUE ("queryId"), CONSTRAINT "PK_afc8a335c8d9d48cefaf1aa2c09" PRIMARY KEY ("id"))`
);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_fromstate_enum" RENAME TO "state_time_log_fromstate_enum_old"`
);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_fromstate_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "fromState" TYPE "public"."state_time_log_fromstate_enum" USING "fromState"::"text"::"public"."state_time_log_fromstate_enum"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_fromstate_enum_old"`);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_tostate_enum" RENAME TO "state_time_log_tostate_enum_old"`
);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_tostate_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "toState" TYPE "public"."state_time_log_tostate_enum" USING "toState"::"text"::"public"."state_time_log_tostate_enum"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_tostate_enum_old"`);
await queryRunner.query(`ALTER TYPE "public"."experiment_state_enum" RENAME TO "experiment_state_enum_old"`);
await queryRunner.query(
`CREATE TYPE "public"."experiment_state_enum" AS ENUM('inactive', 'preview', 'scheduled', 'enrolling', 'enrollmentComplete', 'cancelled', 'archived')`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" DROP DEFAULT`);
await queryRunner.query(
`ALTER TABLE "public"."experiment" ALTER COLUMN "state" TYPE "public"."experiment_state_enum" USING "state"::"text"::"public"."experiment_state_enum"`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" SET DEFAULT 'inactive'`);
await queryRunner.query(`DROP TYPE "public"."experiment_state_enum_old"`);
await queryRunner.query(
`ALTER TABLE "archived_stats" ADD CONSTRAINT "FK_df18fe5ea7298a1dc7bfb33b06f" FOREIGN KEY ("queryId") REFERENCES "query"("id") ON DELETE CASCADE ON UPDATE NO ACTION`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "archived_stats" DROP CONSTRAINT "FK_df18fe5ea7298a1dc7bfb33b06f"`);
await queryRunner.query(
`CREATE TYPE "public"."experiment_state_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" DROP DEFAULT`);
await queryRunner.query(
`ALTER TABLE "public"."experiment" ALTER COLUMN "state" TYPE "public"."experiment_state_enum_old" USING "state"::"text"::"public"."experiment_state_enum_old"`
);
await queryRunner.query(`ALTER TABLE "public"."experiment" ALTER COLUMN "state" SET DEFAULT 'inactive'`);
await queryRunner.query(`DROP TYPE "public"."experiment_state_enum"`);
await queryRunner.query(`ALTER TYPE "public"."experiment_state_enum_old" RENAME TO "experiment_state_enum"`);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_tostate_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "toState" TYPE "public"."state_time_log_tostate_enum_old" USING "toState"::"text"::"public"."state_time_log_tostate_enum_old"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_tostate_enum"`);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_tostate_enum_old" RENAME TO "state_time_log_tostate_enum"`
);
await queryRunner.query(
`CREATE TYPE "public"."state_time_log_fromstate_enum_old" AS ENUM('cancelled', 'enrolling', 'enrollmentComplete', 'inactive', 'preview', 'scheduled')`
);
await queryRunner.query(
`ALTER TABLE "public"."state_time_log" ALTER COLUMN "fromState" TYPE "public"."state_time_log_fromstate_enum_old" USING "fromState"::"text"::"public"."state_time_log_fromstate_enum_old"`
);
await queryRunner.query(`DROP TYPE "public"."state_time_log_fromstate_enum"`);
await queryRunner.query(
`ALTER TYPE "public"."state_time_log_fromstate_enum_old" RENAME TO "state_time_log_fromstate_enum"`
);
await queryRunner.query(`DROP TABLE "archived_stats"`);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ afterEach(() => {
});

describe('MetricRepository Testing', () => {
it('should delete a metric', async () => {
it('should delete a metric', async () => {
createQueryBuilderStub = sandbox.stub(MetricRepository.prototype, 'createQueryBuilder').returns(deleteQueryBuilder);
const result = {
identifiers: [{ id: metric.key }],
Expand Down Expand Up @@ -106,8 +106,9 @@ describe('MetricRepository Testing', () => {
raw: [metric],
};

selectMock.expects('innerJoin').once().returns(selectQueryBuilder);
selectMock.expects('innerJoin').twice().returns(selectQueryBuilder);
selectMock.expects('where').once().returns(selectQueryBuilder);
selectMock.expects('andWhere').once().returns(selectQueryBuilder);
selectMock.expects('getMany').once().returns(Promise.resolve(result));

const res = await repo.findMetricsWithQueries([metric.key]);
Expand All @@ -121,8 +122,9 @@ describe('MetricRepository Testing', () => {
it('should throw an error when get monitored experiment metric by date range fails', async () => {
createQueryBuilderStub = sandbox.stub(MetricRepository.prototype, 'createQueryBuilder').returns(selectQueryBuilder);

selectMock.expects('innerJoin').once().returns(selectQueryBuilder);
selectMock.expects('innerJoin').twice().returns(selectQueryBuilder);
selectMock.expects('where').once().returns(selectQueryBuilder);
selectMock.expects('andWhere').once().returns(selectQueryBuilder);
selectMock.expects('getMany').once().returns(Promise.reject(err));

expect(async () => {
Expand Down
Loading