Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static org.upgradeplatform.utils.Utils.SET_WORKING_GROUP;
import static org.upgradeplatform.utils.Utils.isStringNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -263,7 +264,7 @@ public void failed(Throwable throwable) {

/** @param site This is matched case-insensitively */
public void getExperimentCondition(String site, final ResponseCallback<Assignment> callbacks) {
getExperimentCondition(site, null, callbacks);
getExperimentCondition(site, null, false, callbacks);
}

/**
Expand All @@ -272,11 +273,19 @@ public void getExperimentCondition(String site, final ResponseCallback<Assignmen
*/
public void getExperimentCondition(String site, String target,
final ResponseCallback<Assignment> callbacks) {
getAllExperimentConditions(new ResponseCallback<List<ExperimentsResponse>>() {
@Override
public void onSuccess(@NonNull List<ExperimentsResponse> experiments) {
getExperimentCondition(site, target, false, callbacks);
}

ExperimentsResponse resultExperimentsResponse = findExperimentResponse(site, target, experiments);
/**
* @param site This is matched case-insensitively
* @param target This is matched case-insensitively
* @param ignoreCache If true, always fetch from API for this decision point
*/
public void getExperimentCondition(String site, String target, boolean ignoreCache,
final ResponseCallback<Assignment> callbacks) {
getAllExperimentConditions(site, target, ignoreCache, new ResponseCallback<ExperimentsResponse>() {
@Override
public void onSuccess(@NonNull ExperimentsResponse resultExperimentsResponse) {
Map<String, Factor> assignedFactor = resultExperimentsResponse.getAssignedFactor() != null
? resultExperimentsResponse.getAssignedFactor()[0]
: null;
Expand All @@ -303,7 +312,16 @@ public void onError(@NonNull ErrorResponse error) {
/** @param site This is matched case-insensitively */
public void getAllExperimentConditions(String site,
final ResponseCallback<ExperimentsResponse> callbacks) {
getAllExperimentConditions(site, null, callbacks);
getAllExperimentConditions(site, null, false, callbacks);
}

/**
* @param site This is matched case-insensitively
* @param ignoreCache If true, always fetch from API for this decision point
*/
public void getAllExperimentConditions(String site, boolean ignoreCache,
final ResponseCallback<ExperimentsResponse> callbacks) {
getAllExperimentConditions(site, null, ignoreCache, callbacks);
}

/**
Expand All @@ -312,37 +330,124 @@ public void getAllExperimentConditions(String site,
*/
public void getAllExperimentConditions(String site, String target,
final ResponseCallback<ExperimentsResponse> callbacks) {
getAllExperimentConditions(new ResponseCallback<List<ExperimentsResponse>>() {
@Override
public void onSuccess(@NonNull List<ExperimentsResponse> experiments) {

ExperimentsResponse resultCondition = findExperimentResponse(site, target, experiments);
getAllExperimentConditions(site, target, false, callbacks);
}

/**
* @param site This is matched case-insensitively
* @param target This is matched case-insensitively
* @param ignoreCache If true, always fetch from API for this decision point
*/
public void getAllExperimentConditions(String site, String target, boolean ignoreCache,
final ResponseCallback<ExperimentsResponse> callbacks) {
if (!ignoreCache && this.allExperiments != null) {
Optional<ExperimentsResponse> cachedCondition = findExperimentResponseOptional(site, target,
this.allExperiments);
if (cachedCondition.isPresent()) {
if (callbacks != null) {
callbacks.onSuccess(resultCondition);
callbacks.onSuccess(cachedCondition.get());
}
return;
}
}

@Override
public void onError(@NonNull ErrorResponse error) {
if (callbacks != null)
callbacks.onError(error);
String normalizedTarget = target == null ? "" : target;
ExperimentRequest experimentRequest = new ExperimentRequest(this.context, site, normalizedTarget);
AsyncInvoker invocation = this.apiService.prepareRequest(GET_ALL_EXPERIMENTS);
Entity<ExperimentRequest> requestContent = Entity.json(experimentRequest);
Comment thread
bcb37 marked this conversation as resolved.

}
});
invocation.post(requestContent,
new PublishingRetryCallback<>(invocation, requestContent, MAX_RETRIES, RequestType.POST,
new InvocationCallback<Response>() {

@Override
public void completed(Response response) {
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
response.bufferEntity();
try {
List<ExperimentsResponse> experiments = response
.readEntity(new GenericType<List<ExperimentsResponse>>() {
});
upsertCachedExperiments(experiments);
ExperimentsResponse resultCondition = findExperimentResponse(site, target,
experiments);
if (callbacks != null) {
callbacks.onSuccess(resultCondition);
}
} catch (ProcessingException pe) {
if (callbacks != null) {
callbacks.onError(
new ErrorResponse(pe.getMessage() + "; cause: " + pe.getCause()
+ "; body: " + response.readEntity(String.class)));
}
}
} else {
String status = Response.Status.fromStatusCode(response.getStatus()).toString();
ErrorResponse error = new ErrorResponse(response.getStatus(),
response.readEntity(String.class), status);
if (callbacks != null)
callbacks.onError(error);
}
}

@Override
public void failed(Throwable throwable) {
callbacks.onError(new ErrorResponse(throwable.getMessage()));
}
}));
}

private ExperimentsResponse findExperimentResponse(String site, String target,
private Optional<ExperimentsResponse> findExperimentResponseOptional(String site, String target,
List<ExperimentsResponse> experiments) {
return experiments.stream()
.filter(t -> t.getSite().equalsIgnoreCase(site) &&
(isStringNull(target) ? isStringNull(t.getTarget().toString())
: t.getTarget().toString().equalsIgnoreCase(target)))
.findFirst()
Comment thread
bcb37 marked this conversation as resolved.
.map(ExperimentClient::copyExperimentResponse)
.map(ExperimentClient::copyExperimentResponse);
}

private ExperimentsResponse findExperimentResponse(String site, String target,
List<ExperimentsResponse> experiments) {
return findExperimentResponseOptional(site, target, experiments)
.orElse(new ExperimentsResponse());
}

private void upsertCachedExperiments(List<ExperimentsResponse> fetchedExperiments) {
List<ExperimentsResponse> mergedCache = this.allExperiments == null
? new ArrayList<>()
: new ArrayList<>(this.allExperiments);

if (this.allExperiments == null) {
this.allExperiments = fetchedExperiments.stream()
.map(ExperimentClient::copyExperimentResponse)
.toList();
return;
}

for (ExperimentsResponse fetched : fetchedExperiments) {
String fetchedTarget = fetched.getTarget() == null ? null : fetched.getTarget().toString();
boolean updated = false;
for (int i = 0; i < mergedCache.size(); i++) {
ExperimentsResponse existing = mergedCache.get(i);
String existingTarget = existing.getTarget() == null ? null : existing.getTarget().toString();
if (existing.getSite().equalsIgnoreCase(fetched.getSite()) &&
(isStringNull(existingTarget) ? isStringNull(fetchedTarget)
: existingTarget.equalsIgnoreCase(fetchedTarget))) {
mergedCache.set(i, copyExperimentResponse(fetched));
updated = true;
break;
}
}

if (!updated) {
mergedCache.add(copyExperimentResponse(fetched));
}
}

this.allExperiments = mergedCache;
}

private void rotateConditions(String experimentId) {
if (this.allExperiments == null || isStringNull(experimentId)) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,63 @@
package org.upgradeplatform.requestbeans;

import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class ExperimentRequest {

public static class DecisionPoint {
private String site;
private String target;

public DecisionPoint(String site, String target) {
this.site = site;
this.target = target;
}

public String getSite() {
return site;
}

public void setSite(String site) {
this.site = site;
}

public String getTarget() {
return target;
}

public void setTarget(String target) {
this.target = target;
}
}

private String context;

private DecisionPoint decisionPoint;

public ExperimentRequest(String context) {
super();
this.context = context;
}

public ExperimentRequest(String context, String site, String target) {
super();
this.context = context;
this.decisionPoint = new DecisionPoint(site, target);
}

public String getContext() {
return context;
}

public void setContext(String context) {
this.context = context;
}

public DecisionPoint getDecisionPoint() {
return decisionPoint;
}

public void setDecisionPoint(DecisionPoint decisionPoint) {
this.decisionPoint = decisionPoint;
}
}
11 changes: 11 additions & 0 deletions clientlibs/js/src/ApiService/ApiService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,17 @@ describe('ApiService', () => {

expect(mockHttpClient.doPost).toHaveBeenCalledWith(expectedUrl, requestBody, expectedOptions);
});

it('should call sendRequest with context, site and normalized target', async () => {
const requestBody: UpGradeClientRequests.IGetAllExperimentConditionsRequestBody = {
context: defaultConfig.context,
decisionPoint: { site: 'siteA', target: '' },
};

await apiService.getAllExperimentConditions('siteA', undefined);

expect(mockHttpClient.doPost).toHaveBeenCalledWith(expectedUrl, requestBody, expectedOptions);
});
});

describe('#log', () => {
Expand Down
6 changes: 5 additions & 1 deletion clientlibs/js/src/ApiService/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,15 @@ export default class ApiService {
});
}

public getAllExperimentConditions(): Promise<IExperimentAssignmentv5[]> {
public getAllExperimentConditions(site?: string | null, target?: string | null): Promise<IExperimentAssignmentv5[]> {
const requestBody: UpGradeClientRequests.IGetAllExperimentConditionsRequestBody = {
context: this.context,
};

if (site != null) {
requestBody.decisionPoint = { site, target: target ?? '' };
}
Comment thread
bcb37 marked this conversation as resolved.

return this.sendRequest<IExperimentAssignmentv5[], UpGradeClientRequests.IGetAllExperimentConditionsRequestBody>({
path: this.api.getAllExperimentConditions,
method: UpGradeClientEnums.REQUEST_METHOD.POST,
Expand Down
65 changes: 65 additions & 0 deletions clientlibs/js/src/DataService/DataService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,71 @@ describe('DataService', () => {
});
});

describe('#upsertExperimentAssignmentData', () => {
it('should replace existing decision point assignment', () => {
const cachedAssignments: IExperimentAssignmentv5[] = [
{
site: 'site',
target: 'target',
assignedCondition: [{ conditionCode: 'control', payload: null, id: '1', experimentId: 'exp1' }],
experimentType: EXPERIMENT_TYPE.SIMPLE,
},
];

const fetchedAssignments: IExperimentAssignmentv5[] = [
{
site: 'site',
target: 'target',
assignedCondition: [{ conditionCode: 'variant', payload: null, id: '2', experimentId: 'exp1' }],
experimentType: EXPERIMENT_TYPE.SIMPLE,
},
];

dataService.setExperimentAssignmentData(cachedAssignments);
dataService.upsertExperimentAssignmentData(fetchedAssignments);

expect(dataService.getExperimentAssignmentData()).toEqual(fetchedAssignments);
});

it('should preserve unrelated decision points while appending new one', () => {
const cachedAssignments: IExperimentAssignmentv5[] = [
{
site: 'siteA',
target: 'targetA',
assignedCondition: [{ conditionCode: 'control', payload: null, id: '1', experimentId: 'expA' }],
experimentType: EXPERIMENT_TYPE.SIMPLE,
},
];

const fetchedAssignments: IExperimentAssignmentv5[] = [
{
site: 'siteB',
target: 'targetB',
assignedCondition: [{ conditionCode: 'variant', payload: null, id: '2', experimentId: 'expB' }],
experimentType: EXPERIMENT_TYPE.SIMPLE,
},
];

dataService.setExperimentAssignmentData(cachedAssignments);
dataService.upsertExperimentAssignmentData(fetchedAssignments);

expect(dataService.getExperimentAssignmentData()).toEqual([
{
site: 'siteA',
target: 'targetA',
assignedCondition: [{ conditionCode: 'control', payload: null, id: '1', experimentId: 'expA' }],
experimentType: EXPERIMENT_TYPE.SIMPLE,
},
{
site: 'siteB',
target: 'targetB',
assignedCondition: [{ conditionCode: 'variant', payload: null, id: '2', experimentId: 'expB' }],
experimentType: EXPERIMENT_TYPE.SIMPLE,
},
]);
});
});

describe('#getFeatureFlags', () => {
it('should return the feature flags', () => {
const featureFlagsKeys: string[] = ['testFlagKey'];
Expand Down
Loading