Skip to content

Commit

Permalink
support for caliper graded profile (#759)
Browse files Browse the repository at this point in the history
* support for caliper graded profile

* add iso duration

* remove unused imports

* export interfaces

* add caliper endpoint to v1 api

* Revert "add iso duration"

This reverts commit be6677b.

* stricter typing

* add caliper actor

* error handling for unsupported profiles and events

* update caliper data object

* add to v1

* add caliper enums to interfaces

* resolve conflicts

* add duration package

* match log file

* update version

* use upgrade types

* resolve conflicts

* update lockfile
  • Loading branch information
jreddig authored Apr 24, 2023
1 parent 5f4f854 commit adcaf6a
Show file tree
Hide file tree
Showing 13 changed files with 848 additions and 17,868 deletions.
18,421 changes: 553 additions & 17,868 deletions backend/packages/Upgrade/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/packages/Upgrade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
"figlet": "^1.2.4",
"google-auth-library": "^8.5.1",
"helmet": "^3.21.2",
"iso8601-duration": "^2.1.1",
"jest": "^26.6.3",
"json-diff": "^0.5.4",
"jsonfile": "^5.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { Metric } from '../models/Metric';
import * as express from 'express';
import { AppRequest } from '../../types';
import { env } from '../../env';
import flatten from 'lodash.flatten';
import { CaliperLogEnvelope } from './validators/CaliperLogEnvelope';

interface IExperimentAssignment {
expId: string;
Expand Down Expand Up @@ -650,6 +652,54 @@ export class ExperimentClientController {
});
}


/**
* @swagger
* /log/caliper:
* post:
* description: Post Caliper format log data
* consumes:
* - application/json
* parameters:
* - in: body
* name: data
* required: true
* description: User Document
* tags:
* - Client Side SDK
* produces:
* - application/json
* responses:
* '200':
* description: Log data
* '500':
* description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint
*/
@Post('log/caliper')
public async caliperLog(
@Body({ validate: { validationError: { target: false, value: false } } })
@Req()
request: AppRequest,
envelope: CaliperLogEnvelope
): Promise<Log[]> {
let result = envelope.data.map(async log => {
// getOriginalUserDoc call for alias
const experimentUserDoc = await this.getUserDoc(log.object.assignee.id, request.logger);
if (experimentUserDoc) {
// append userDoc in logger
request.logger.child({ userDoc: experimentUserDoc });
request.logger.info({ message: 'Got the original user doc' });
}
return this.experimentAssignmentService.caliperDataLog(log, {
logger: request.logger,
userDoc: experimentUserDoc,
});
});

const logsToReturn = await Promise.all(result);
return flatten(logsToReturn);
}

/**
* @swagger
* /bloblog:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ import { AppRequest } from '../../types';
import { env } from '../../env';
import { MonitoredDecisionPointLog } from '../models/MonitoredDecisionPointLog';
import { Log } from '../models/Log';
import flatten from 'lodash.flatten';
import { CaliperLogEnvelope } from './validators/CaliperLogEnvelope';

interface IMonitoredDeciosionPoint {
id: string;
Expand Down Expand Up @@ -616,6 +618,53 @@ export class ExperimentClientController {
});
}

/**
* @swagger
* /log/caliper:
* post:
* description: Post Caliper format log data
* consumes:
* - application/json
* parameters:
* - in: body
* name: data
* required: true
* description: User Document
* tags:
* - Client Side SDK
* produces:
* - application/json
* responses:
* '200':
* description: Log data
* '500':
* description: null value in column "id\" of relation \"experiment_user\" violates not-null constraint
*/
@Post('log/caliper')
public async caliperLog(
@Body({ validate: { validationError: { target: false, value: false } } })
@Req()
request: AppRequest,
envelope: CaliperLogEnvelope
): Promise<Log[]> {
let result = envelope.data.map(async log => {
// getOriginalUserDoc call for alias
const experimentUserDoc = await this.getUserDoc(log.object.assignee.id, request.logger);
if (experimentUserDoc) {
// append userDoc in logger
request.logger.child({ userDoc: experimentUserDoc });
request.logger.info({ message: 'Got the original user doc' });
}
return this.experimentAssignmentService.caliperDataLog(log, {
logger: request.logger,
userDoc: experimentUserDoc,
});
});

const logsToReturn = await Promise.all(result);
return flatten(logsToReturn);
}

/**
* @swagger
* /bloblog:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { IsNotEmpty, IsDefined, IsString, IsJSON, IsObject } from 'class-validator';
import { Attempt, CaliperActor, ScoreObject, SUPPORTED_CALIPER_EVENTS, SUPPORTED_CALIPER_PROFILES } from 'upgrade_types';

export class CaliperLogData {

@IsDefined()
@IsNotEmpty()
@IsString()
public profile: SUPPORTED_CALIPER_PROFILES;

@IsDefined()
@IsNotEmpty()
@IsString()
public type: SUPPORTED_CALIPER_EVENTS;

@IsDefined()
@IsNotEmpty()
@IsJSON()
public actor: CaliperActor;

@IsDefined()
@IsNotEmpty()
@IsString()
public action: string;

@IsDefined()
@IsNotEmpty()
@IsString()
public eventTime: string;

@IsDefined()
@IsNotEmpty()
@IsJSON()
public object: Attempt;

@IsObject()
public extensions: Record<string, unknown>;

@IsObject()
@IsNotEmpty()
@IsJSON()
public generated: ScoreObject;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IsNotEmpty, IsDefined, IsString } from 'class-validator';
import { CaliperLogData } from './CaliperLogData';


export class CaliperLogEnvelope {
@IsDefined()
@IsNotEmpty()
@IsString()
public sensor: string;

@IsDefined()
@IsNotEmpty()
@IsString()
public sendTime: string;

@IsDefined()
@IsNotEmpty()
@IsString()
public dataVersion: string;

public data: CaliperLogData[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export class ErrorHandlerMiddleware implements ExpressErrorMiddlewareInterface {
message = error.message;
type = SERVER_ERROR.QUERY_FAILED;
break;
case 422:
message = error.message;
type = SERVER_ERROR.UNSUPPORTED_CALIPER
break;
default:
message = error.message;
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
EXCLUSION_CODE,
MARKED_DECISION_POINT_STATUS,
EXPERIMENT_TYPE,
SUPPORTED_CALIPER_PROFILES,
SUPPORTED_CALIPER_EVENTS,
IExperimentAssignmentv4,
} from 'upgrade_types';
import { IndividualExclusionRepository } from '../repositories/IndividualExclusionRepository';
Expand Down Expand Up @@ -58,6 +60,8 @@ import { AnalyticsRepository } from '../repositories/AnalyticsRepository';
import { Segment } from '../models/Segment';
import { ConditionPayloadRepository } from '../repositories/ConditionPayloadRepository';
import { In } from 'typeorm';
import { CaliperLogData } from '../controllers/validators/CaliperLogData';
import { parse, toSeconds } from 'iso8601-duration';
import { FactorDTO } from '../DTO/FactorDTO';
import { ConditionPayloadDTO } from '../DTO/ConditionPayloadDTO';
@Service()
Expand Down Expand Up @@ -826,6 +830,30 @@ export class ExperimentAssignmentService {
return flatten(logsToReturn);
}

public async caliperDataLog(log: CaliperLogData, requestContext: { logger: UpgradeLogger; userDoc: any }): Promise<Log[]>{
if (log.profile === SUPPORTED_CALIPER_PROFILES.GRADING && log.type === SUPPORTED_CALIPER_EVENTS.GRADE) {
requestContext.logger.info({ message: 'Starting the Caliper log call for user' });
const userId = log.object.assignee.id

const logs: ILogInput = log.generated.attempt.extensions;

logs.metrics.attributes['duration'] = toSeconds(parse(log.generated.attempt.duration));
logs.metrics.attributes['scoreGiven'] = log.generated.scoreGiven;

return this.dataLog(userId, [logs], {
logger: requestContext.logger,
userDoc: requestContext.userDoc,
})
}
else {
let error = new Error(`Unsupported Caliper profile: ${log.profile} or type: ${log.type}`);
(error as any).type = SERVER_ERROR.UNSUPPORTED_CALIPER;
(error as any).httpCode = 422;
throw error;
}

}

public async dataLog(
userId: string,
jsonLog: ILogInput[],
Expand Down
10 changes: 10 additions & 0 deletions clientlibs/js/src/UpgradeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
ILogInput,
MARKED_DECISION_POINT_STATUS,
IExperimentAssignmentv4,
CaliperEnvelope
} from 'upgrade_types';
import getExperimentCondition, { Assignment } from './functions/getExperimentCondition';
import markExperimentPoint from './functions/markExperimentPoint';
import getAllFeatureFlags from './functions/getAllfeatureFlags';
import log from './functions/log';
import logCaliper from './functions/logCaliper';
import setAltUserIds from './functions/setAltUserIds';
import addMetrics from './functions/addMetrics';
import getFeatureFlag from './functions/getFeatureFlag';
Expand All @@ -31,6 +33,7 @@ export default class UpgradeClient {
failedExperimentPoint: '',
getAllFeatureFlag: '',
log: '',
logCaliper: '',
altUserIds: '',
addMetrics: '',
};
Expand Down Expand Up @@ -66,6 +69,7 @@ export default class UpgradeClient {
failedExperimentPoint: `${hostUrl}/api/v4/failed`,
getAllFeatureFlag: `${hostUrl}/api/v4/featureflag`,
log: `${hostUrl}/api/v4/log`,
logCaliper: `${hostUrl}/api/v4/logCaliper`,
altUserIds: `${hostUrl}/api/v4/useraliases`,
addMetrics: `${hostUrl}/api/v4/metric`,
};
Expand Down Expand Up @@ -188,6 +192,12 @@ export default class UpgradeClient {
return await log(this.api.log, this.userId, this.token, this.clientSessionId, value, sendAsAnalytics);
}


async logCaliper(value: CaliperEnvelope, sendAsAnalytics = false): Promise<Interfaces.ILog[]> {
this.validateClient();
return await logCaliper(this.api.logCaliper, this.userId, this.token, this.clientSessionId, value, sendAsAnalytics);
}

async setAltUserIds(altUserIds: string[]): Promise<Interfaces.IExperimentUserAliases[]> {
this.validateClient();
return await setAltUserIds(this.api.altUserIds, this.userId, this.token, this.clientSessionId, altUserIds);
Expand Down
27 changes: 27 additions & 0 deletions clientlibs/js/src/functions/logCaliper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Types, Interfaces } from '../identifiers';
import fetchDataService from '../common/fetchDataService';
import { CaliperEnvelope } from 'upgrade_types';

export default async function logCaliper(
url: string,
userId: string,
token: string,
clientSessionId: string,
value: CaliperEnvelope,
sendAsAnalytics = false
): Promise<Interfaces.ILog[]> {
const logResponse = await fetchDataService(
url,
token,
clientSessionId,
value,
Types.REQUEST_TYPES.POST,
sendAsAnalytics
);
if (logResponse.status) {
return logResponse.data;
} else {
throw new Error(JSON.stringify(logResponse.message));
}

}
9 changes: 9 additions & 0 deletions types/src/Experiment/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export enum SERVER_ERROR {
CONDITION_NOT_FOUND = 'Condition not found',
EXPERIMENT_ID_MISSING_FOR_SHARED_DECISIONPOINT = 'Experiment ID not provided for shared Decision Point',
INVALID_EXPERIMENT_ID_FOR_SHARED_DECISIONPOINT = 'Experiment ID provided is invalid for shared Decision Point',
UNSUPPORTED_CALIPER = 'Caliper profile or event not supported'
}

export enum MARKED_DECISION_POINT_STATUS {
Expand Down Expand Up @@ -175,3 +176,11 @@ export enum PAYLOAD_TYPE {
JSON = 'json',
CSV = 'csv',
}

export enum SUPPORTED_CALIPER_PROFILES {
GRADING = "GradingProfile"
}

export enum SUPPORTED_CALIPER_EVENTS {
GRADE = "GradeEvent"
}
Loading

0 comments on commit adcaf6a

Please sign in to comment.