feat(project): create v8 api for segments support (#11300)
Because * We need to extend experiments to support segment functionality * We want to be able to update the API for Jetstream without affecting the clients that consume v6 This commit * Creates a new v8 api based on the existing v6 api to support adding segments Fixes #11290
This commit is contained in:
Родитель
c8b14364b5
Коммит
98f4409a43
|
@ -1993,6 +1993,348 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/experiments/": {
|
||||
"get": {
|
||||
"operationId": "listNimbusExperiments",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "status",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Draft",
|
||||
"Preview",
|
||||
"Live",
|
||||
"Complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/experiments/{slug}/": {
|
||||
"get": {
|
||||
"operationId": "retrieveNimbusExperiment",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "status",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Draft",
|
||||
"Preview",
|
||||
"Live",
|
||||
"Complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/draft-experiments/": {
|
||||
"get": {
|
||||
"operationId": "listNimbusExperiments",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/draft-experiments/{slug}/": {
|
||||
"get": {
|
||||
"operationId": "retrieveNimbusExperiment",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/experiments/{slug}/intent-to-ship-email": {
|
||||
"put": {
|
||||
"operationId": "updateExperiment",
|
||||
|
@ -3845,36 +4187,8 @@
|
|||
"readOnly": true
|
||||
},
|
||||
"branches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 80,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$"
|
||||
},
|
||||
"ratio": {
|
||||
"type": "integer",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0
|
||||
},
|
||||
"features": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"screenshots": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"targeting": {
|
||||
"type": "string",
|
||||
|
@ -3908,11 +4222,11 @@
|
|||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"locales": {
|
||||
"localizations": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"localizations": {
|
||||
"locales": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
|
@ -3925,7 +4239,6 @@
|
|||
"slug",
|
||||
"channel",
|
||||
"bucketConfig",
|
||||
"branches",
|
||||
"startDate",
|
||||
"enrollmentEndDate",
|
||||
"endDate",
|
||||
|
|
|
@ -2005,6 +2005,348 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/experiments/": {
|
||||
"get": {
|
||||
"operationId": "listNimbusExperiments",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "status",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Draft",
|
||||
"Preview",
|
||||
"Live",
|
||||
"Complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/experiments/{slug}/": {
|
||||
"get": {
|
||||
"operationId": "retrieveNimbusExperiment",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "status",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"Draft",
|
||||
"Preview",
|
||||
"Live",
|
||||
"Complete"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/draft-experiments/": {
|
||||
"get": {
|
||||
"operationId": "listNimbusExperiments",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v8/draft-experiments/{slug}/": {
|
||||
"get": {
|
||||
"operationId": "retrieveNimbusExperiment",
|
||||
"description": "",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "slug",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"description": "",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_localized",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_localized",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "is_first_run",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "is_first_run",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "application",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "application",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"firefox-desktop",
|
||||
"fenix",
|
||||
"ios",
|
||||
"focus-android",
|
||||
"klar-android",
|
||||
"focus-ios",
|
||||
"klar-ios",
|
||||
"monitor-web",
|
||||
"vpn-web",
|
||||
"fxa-web",
|
||||
"demo-app"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "feature_config",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "feature_config",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NimbusExperiment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Nimbus: Public Analysis"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v2/experiments/{slug}/intent-to-ship-email": {
|
||||
"put": {
|
||||
"operationId": "updateExperiment",
|
||||
|
@ -3857,36 +4199,8 @@
|
|||
"readOnly": true
|
||||
},
|
||||
"branches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"maxLength": 80,
|
||||
"pattern": "^[-a-zA-Z0-9_]+$"
|
||||
},
|
||||
"ratio": {
|
||||
"type": "integer",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0
|
||||
},
|
||||
"features": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"screenshots": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"targeting": {
|
||||
"type": "string",
|
||||
|
@ -3920,11 +4234,11 @@
|
|||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"locales": {
|
||||
"localizations": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
"localizations": {
|
||||
"locales": {
|
||||
"type": "string",
|
||||
"readOnly": true
|
||||
},
|
||||
|
@ -3937,7 +4251,6 @@
|
|||
"slug",
|
||||
"channel",
|
||||
"bucketConfig",
|
||||
"branches",
|
||||
"startDate",
|
||||
"enrollmentEndDate",
|
||||
"endDate",
|
||||
|
|
|
@ -33,6 +33,9 @@ class Command(BaseCommand):
|
|||
elif "/api/v6/" in path:
|
||||
for method in paths[path]:
|
||||
paths[path][method]["tags"] = ["Nimbus: Public"]
|
||||
elif "/api/v8/" in path:
|
||||
for method in paths[path]:
|
||||
paths[path][method]["tags"] = ["Nimbus: Public Analysis"]
|
||||
|
||||
return json.dumps(schema, indent=2)
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ class NimbusExperimentViewSet(
|
|||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_class = NimbusExperimentFilterSet
|
||||
|
||||
@method_decorator(cache_page(settings.V6_API_CACHE_DURATION))
|
||||
@method_decorator(cache_page(settings.API_CACHE_DURATION))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
|
|
@ -46,6 +46,6 @@ class NimbusExperimentViewSet(
|
|||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_class = NimbusExperimentFilterSet
|
||||
|
||||
@method_decorator(cache_page(settings.V6_API_CACHE_DURATION))
|
||||
@method_decorator(cache_page(settings.API_CACHE_DURATION))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
import contextlib
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from rest_framework import serializers
|
||||
|
||||
from experimenter.experiments.models import (
|
||||
NimbusBranch,
|
||||
NimbusBucketRange,
|
||||
NimbusExperiment,
|
||||
)
|
||||
|
||||
|
||||
class NimbusBucketRangeSerializer(serializers.ModelSerializer):
|
||||
randomizationUnit = serializers.ReadOnlyField(
|
||||
source="isolation_group.randomization_unit"
|
||||
)
|
||||
namespace = serializers.ReadOnlyField(source="isolation_group.namespace")
|
||||
total = serializers.ReadOnlyField(source="isolation_group.total")
|
||||
|
||||
class Meta:
|
||||
model = NimbusBucketRange
|
||||
fields = (
|
||||
"randomizationUnit",
|
||||
"namespace",
|
||||
"start",
|
||||
"count",
|
||||
"total",
|
||||
)
|
||||
|
||||
|
||||
class NimbusBranchSerializer(serializers.ModelSerializer):
|
||||
features = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = NimbusBranch
|
||||
fields = ("slug", "ratio", "features")
|
||||
|
||||
def get_features(self, obj):
|
||||
features = []
|
||||
for fv in obj.feature_values.all():
|
||||
feature_value = {
|
||||
"featureId": fv.feature_config and fv.feature_config.slug or "",
|
||||
"enabled": True, # TODO: Remove after Desktop 104 is no longer supported
|
||||
"value": {},
|
||||
}
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
# The value may still be invalid at this time
|
||||
feature_value["value"] = json.loads(fv.value)
|
||||
|
||||
features.append(feature_value)
|
||||
return features
|
||||
|
||||
|
||||
class NimbusBranchSerializerDesktop(NimbusBranchSerializer):
|
||||
feature = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = NimbusBranch
|
||||
fields = ("slug", "ratio", "feature", "features")
|
||||
|
||||
def get_feature(self, obj):
|
||||
return {
|
||||
"featureId": "this-is-included-for-desktop-pre-95-support",
|
||||
"enabled": False, # TODO: Remove after Desktop 104 is no longer supported
|
||||
"value": {},
|
||||
}
|
||||
|
||||
|
||||
class NimbusBranchSerializerMobile(NimbusBranchSerializer):
|
||||
feature = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = NimbusBranch
|
||||
fields = ("slug", "ratio", "feature", "features")
|
||||
|
||||
def get_feature(self, obj):
|
||||
return {
|
||||
"featureId": "this-is-included-for-mobile-pre-96-support",
|
||||
"enabled": False, # TODO: Remove after Desktop 104 is no longer supported
|
||||
"value": {},
|
||||
}
|
||||
|
||||
|
||||
class NimbusExperimentSerializer(serializers.ModelSerializer):
|
||||
schemaVersion = serializers.ReadOnlyField(default=settings.NIMBUS_SCHEMA_VERSION)
|
||||
id = serializers.ReadOnlyField(source="slug")
|
||||
arguments = serializers.ReadOnlyField(default={})
|
||||
application = serializers.SerializerMethodField()
|
||||
appName = serializers.SerializerMethodField()
|
||||
appId = serializers.SerializerMethodField()
|
||||
branches = serializers.SerializerMethodField()
|
||||
userFacingName = serializers.ReadOnlyField(source="name")
|
||||
userFacingDescription = serializers.ReadOnlyField(source="public_description")
|
||||
isEnrollmentPaused = serializers.ReadOnlyField(source="is_paused")
|
||||
isRollout = serializers.ReadOnlyField(source="is_rollout")
|
||||
bucketConfig = NimbusBucketRangeSerializer(source="bucket_range")
|
||||
featureIds = serializers.SerializerMethodField()
|
||||
probeSets = serializers.ReadOnlyField(default=[])
|
||||
outcomes = serializers.SerializerMethodField()
|
||||
startDate = serializers.DateField(source="start_date")
|
||||
enrollmentEndDate = serializers.DateField(source="actual_enrollment_end_date")
|
||||
endDate = serializers.DateField(source="end_date")
|
||||
proposedDuration = serializers.ReadOnlyField(source="proposed_duration")
|
||||
proposedEnrollment = serializers.ReadOnlyField(source="proposed_enrollment")
|
||||
referenceBranch = serializers.SerializerMethodField()
|
||||
featureValidationOptOut = serializers.ReadOnlyField(
|
||||
source="is_client_schema_disabled"
|
||||
)
|
||||
localizations = serializers.SerializerMethodField()
|
||||
locales = serializers.SerializerMethodField()
|
||||
publishedDate = serializers.DateTimeField(source="published_date")
|
||||
|
||||
class Meta:
|
||||
model = NimbusExperiment
|
||||
fields = (
|
||||
"schemaVersion",
|
||||
"slug",
|
||||
"id",
|
||||
"arguments",
|
||||
"application",
|
||||
"appName",
|
||||
"appId",
|
||||
"channel",
|
||||
"userFacingName",
|
||||
"userFacingDescription",
|
||||
"isEnrollmentPaused",
|
||||
"isRollout",
|
||||
"bucketConfig",
|
||||
"featureIds",
|
||||
"probeSets",
|
||||
"outcomes",
|
||||
"branches",
|
||||
"targeting",
|
||||
"startDate",
|
||||
"enrollmentEndDate",
|
||||
"endDate",
|
||||
"proposedDuration",
|
||||
"proposedEnrollment",
|
||||
"referenceBranch",
|
||||
"featureValidationOptOut",
|
||||
"localizations",
|
||||
"locales",
|
||||
"publishedDate",
|
||||
)
|
||||
|
||||
def get_application(self, obj):
|
||||
return self.get_appId(obj)
|
||||
|
||||
def get_appName(self, obj):
|
||||
return obj.application_config.app_name
|
||||
|
||||
def get_appId(self, obj):
|
||||
return obj.application_config.channel_app_id.get(obj.channel, "")
|
||||
|
||||
def get_branches(self, obj):
|
||||
serializer_cls = NimbusBranchSerializer
|
||||
if obj.application == NimbusExperiment.Application.DESKTOP:
|
||||
serializer_cls = NimbusBranchSerializerDesktop
|
||||
elif NimbusExperiment.Application.is_mobile(obj.application):
|
||||
serializer_cls = NimbusBranchSerializerMobile
|
||||
|
||||
return serializer_cls(obj.branches.all(), many=True).data
|
||||
|
||||
def get_featureIds(self, obj):
|
||||
return sorted(
|
||||
[feature_config.slug for feature_config in obj.feature_configs.all()]
|
||||
)
|
||||
|
||||
def get_outcomes(self, obj):
|
||||
prioritized_outcomes = (
|
||||
("primary", obj.primary_outcomes),
|
||||
("secondary", obj.secondary_outcomes),
|
||||
)
|
||||
return [
|
||||
{"slug": slug, "priority": priority}
|
||||
for (priority, outcomes) in prioritized_outcomes
|
||||
for slug in outcomes
|
||||
]
|
||||
|
||||
def get_referenceBranch(self, obj):
|
||||
if obj.reference_branch:
|
||||
return obj.reference_branch.slug
|
||||
|
||||
def get_localizations(self, obj):
|
||||
if obj.is_localized:
|
||||
with contextlib.suppress(json.JSONDecodeError):
|
||||
return json.loads(obj.localizations)
|
||||
|
||||
def get_locales(self, obj):
|
||||
locale_codes = [locale.code for locale in obj.locales.all()]
|
||||
if len(locale_codes):
|
||||
return locale_codes
|
|
@ -0,0 +1,15 @@
|
|||
from rest_framework.routers import SimpleRouter
|
||||
|
||||
from experimenter.experiments.api.v8.views import (
|
||||
NimbusExperimentDraftViewSet,
|
||||
NimbusExperimentViewSet,
|
||||
)
|
||||
|
||||
router = SimpleRouter()
|
||||
router.register(r"experiments", NimbusExperimentViewSet, "nimbus-experiment-rest-v8")
|
||||
router.register(
|
||||
r"draft-experiments",
|
||||
NimbusExperimentDraftViewSet,
|
||||
"nimbus-experiment-rest-v8-draft",
|
||||
)
|
||||
urlpatterns = router.urls
|
|
@ -0,0 +1,78 @@
|
|||
from django.conf import settings
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.cache import cache_page
|
||||
from django_filters import FilterSet, filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import mixins, viewsets
|
||||
|
||||
from experimenter.experiments.api.v8.serializers import NimbusExperimentSerializer
|
||||
from experimenter.experiments.models import NimbusExperiment, NimbusFeatureConfig
|
||||
|
||||
|
||||
class BaseExperimentFilterSet(FilterSet):
|
||||
application = filters.MultipleChoiceFilter(
|
||||
choices=NimbusExperiment.Application.choices,
|
||||
)
|
||||
|
||||
feature_config = filters.ModelMultipleChoiceFilter(
|
||||
queryset=NimbusFeatureConfig.objects.all(),
|
||||
field_name="feature_configs__slug",
|
||||
to_field_name="slug",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = NimbusExperiment
|
||||
fields = ("is_localized",)
|
||||
|
||||
|
||||
class NimbusExperimentFilterSet(BaseExperimentFilterSet):
|
||||
class Meta:
|
||||
model = NimbusExperiment
|
||||
fields = (*BaseExperimentFilterSet.Meta.fields, "is_first_run", "status")
|
||||
|
||||
|
||||
class NimbusDraftExperimentFilterSet(BaseExperimentFilterSet):
|
||||
class Meta:
|
||||
model = NimbusExperiment
|
||||
fields = (*BaseExperimentFilterSet.Meta.fields, "is_first_run")
|
||||
|
||||
|
||||
class NimbusExperimentViewSet(
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
lookup_field = "slug"
|
||||
queryset = (
|
||||
NimbusExperiment.objects.with_related()
|
||||
.exclude(status__in=[NimbusExperiment.Status.DRAFT])
|
||||
.order_by("slug")
|
||||
)
|
||||
serializer_class = NimbusExperimentSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_class = NimbusExperimentFilterSet
|
||||
|
||||
@method_decorator(cache_page(settings.API_CACHE_DURATION))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class NimbusExperimentDraftViewSet(NimbusExperimentViewSet):
|
||||
filterset_class = NimbusDraftExperimentFilterSet
|
||||
|
||||
queryset = (
|
||||
NimbusExperiment.objects.with_related()
|
||||
.filter(status=NimbusExperiment.Status.DRAFT)
|
||||
.order_by("slug")
|
||||
)
|
||||
|
||||
|
||||
class NimbusExperimentFirstRunViewSet(NimbusExperimentViewSet):
|
||||
filterset_class = BaseExperimentFilterSet
|
||||
|
||||
queryset = (
|
||||
NimbusExperiment.objects.with_related()
|
||||
.filter(status=NimbusExperiment.Status.LIVE)
|
||||
.filter(is_first_run=True)
|
||||
.order_by("slug")
|
||||
)
|
|
@ -0,0 +1,372 @@
|
|||
import datetime
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from mozilla_nimbus_shared import check_schema
|
||||
from parameterized import parameterized
|
||||
|
||||
from experimenter.base.tests.factories import LocaleFactory
|
||||
from experimenter.experiments.api.v8.serializers import NimbusExperimentSerializer
|
||||
from experimenter.experiments.models import NimbusBranchFeatureValue, NimbusExperiment
|
||||
from experimenter.experiments.tests.factories import (
|
||||
TEST_LOCALIZATIONS,
|
||||
NimbusBranchFactory,
|
||||
NimbusExperimentFactory,
|
||||
NimbusFeatureConfigFactory,
|
||||
)
|
||||
|
||||
|
||||
class TestNimbusExperimentSerializer(TestCase):
|
||||
maxDiff = None
|
||||
|
||||
def test_expected_schema_with_desktop(self):
|
||||
locale_en_us = LocaleFactory.create(code="en-US")
|
||||
application = NimbusExperiment.Application.DESKTOP
|
||||
feature1 = NimbusFeatureConfigFactory.create(application=application)
|
||||
feature2 = NimbusFeatureConfigFactory.create(application=application)
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||
application=application,
|
||||
firefox_min_version=NimbusExperiment.MIN_REQUIRED_VERSION,
|
||||
feature_configs=[feature1, feature2],
|
||||
targeting_config_slug=NimbusExperiment.TargetingConfig.NO_TARGETING,
|
||||
channel=NimbusExperiment.Channel.NIGHTLY,
|
||||
primary_outcomes=["foo", "bar", "baz"],
|
||||
secondary_outcomes=["quux", "xyzzy"],
|
||||
locales=[locale_en_us],
|
||||
_enrollment_end_date=datetime.date(2022, 1, 5),
|
||||
)
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
experiment_data = serializer.data.copy()
|
||||
bucket_data = dict(experiment_data.pop("bucketConfig"))
|
||||
branches_data = [dict(b) for b in experiment_data.pop("branches")]
|
||||
feature_ids_data = experiment_data.pop("featureIds")
|
||||
|
||||
assert experiment.start_date
|
||||
assert experiment.actual_enrollment_end_date
|
||||
assert experiment.end_date
|
||||
|
||||
min_required_version = NimbusExperiment.MIN_REQUIRED_VERSION
|
||||
|
||||
self.assertDictEqual(
|
||||
experiment_data,
|
||||
{
|
||||
"arguments": {},
|
||||
"application": "firefox-desktop",
|
||||
"appName": "firefox_desktop",
|
||||
"appId": "firefox-desktop",
|
||||
"channel": "nightly",
|
||||
# DRF manually replaces the isoformat suffix so we have to do the same
|
||||
"startDate": experiment.start_date.isoformat().replace("+00:00", "Z"),
|
||||
"enrollmentEndDate": (
|
||||
experiment.actual_enrollment_end_date.isoformat().replace(
|
||||
"+00:00", "Z"
|
||||
)
|
||||
),
|
||||
"endDate": experiment.end_date.isoformat().replace("+00:00", "Z"),
|
||||
"id": experiment.slug,
|
||||
"isEnrollmentPaused": True,
|
||||
"isRollout": False,
|
||||
"proposedDuration": experiment.proposed_duration,
|
||||
"proposedEnrollment": experiment.proposed_enrollment,
|
||||
"referenceBranch": experiment.reference_branch.slug,
|
||||
"schemaVersion": settings.NIMBUS_SCHEMA_VERSION,
|
||||
"slug": experiment.slug,
|
||||
"targeting": (
|
||||
f'(browserSettings.update.channel == "nightly") '
|
||||
f"&& (version|versionCompare('{min_required_version}') >= 0) "
|
||||
f"&& (locale in ['en-US'])"
|
||||
),
|
||||
"userFacingDescription": experiment.public_description,
|
||||
"userFacingName": experiment.name,
|
||||
"probeSets": [],
|
||||
"outcomes": [
|
||||
{"priority": "primary", "slug": "foo"},
|
||||
{"priority": "primary", "slug": "bar"},
|
||||
{"priority": "primary", "slug": "baz"},
|
||||
{"priority": "secondary", "slug": "quux"},
|
||||
{"priority": "secondary", "slug": "xyzzy"},
|
||||
],
|
||||
"featureValidationOptOut": experiment.is_client_schema_disabled,
|
||||
"localizations": None,
|
||||
"locales": ["en-US"],
|
||||
"publishedDate": experiment.published_date,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(set(feature_ids_data), {feature1.slug, feature2.slug})
|
||||
|
||||
self.assertEqual(
|
||||
bucket_data,
|
||||
{
|
||||
"randomizationUnit": (
|
||||
experiment.bucket_range.isolation_group.randomization_unit
|
||||
),
|
||||
"namespace": experiment.bucket_range.isolation_group.namespace,
|
||||
"start": experiment.bucket_range.start,
|
||||
"count": experiment.bucket_range.count,
|
||||
"total": experiment.bucket_range.isolation_group.total,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(len(branches_data), 2)
|
||||
for branch in experiment.branches.all():
|
||||
self.assertIn(
|
||||
{
|
||||
"slug": branch.slug,
|
||||
"ratio": branch.ratio,
|
||||
"feature": {
|
||||
"featureId": "this-is-included-for-desktop-pre-95-support",
|
||||
"enabled": False,
|
||||
"value": {},
|
||||
},
|
||||
"features": [
|
||||
{
|
||||
"featureId": fv.feature_config.slug,
|
||||
"enabled": True,
|
||||
"value": json.loads(fv.value),
|
||||
}
|
||||
for fv in branch.feature_values.all()
|
||||
],
|
||||
},
|
||||
branches_data,
|
||||
)
|
||||
|
||||
check_schema("experiments/NimbusExperiment", serializer.data)
|
||||
|
||||
def test_enrollment_end_date_none_while_live_enrolling(self):
|
||||
locale_en_us = LocaleFactory.create(code="en-US")
|
||||
application = NimbusExperiment.Application.DESKTOP
|
||||
feature1 = NimbusFeatureConfigFactory.create(application=application)
|
||||
feature2 = NimbusFeatureConfigFactory.create(application=application)
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.LIVE_APPROVE_APPROVE,
|
||||
application=application,
|
||||
firefox_min_version=NimbusExperiment.MIN_REQUIRED_VERSION,
|
||||
feature_configs=[feature1, feature2],
|
||||
targeting_config_slug=NimbusExperiment.TargetingConfig.NO_TARGETING,
|
||||
channel=NimbusExperiment.Channel.NIGHTLY,
|
||||
primary_outcomes=["foo", "bar", "baz"],
|
||||
secondary_outcomes=["quux", "xyzzy"],
|
||||
locales=[locale_en_us],
|
||||
)
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
experiment_data = serializer.data.copy()
|
||||
|
||||
assert experiment.start_date
|
||||
self.assertIsNone(experiment.actual_enrollment_end_date)
|
||||
self.assertIsNone(experiment.end_date)
|
||||
|
||||
self.assertEqual(
|
||||
experiment_data.get("enrollmentEndDate"),
|
||||
experiment.actual_enrollment_end_date,
|
||||
)
|
||||
|
||||
def test_list_includes_single_and_multi_feature_schemas(self):
|
||||
feature1 = NimbusFeatureConfigFactory.create()
|
||||
feature2 = NimbusFeatureConfigFactory.create()
|
||||
single_feature_experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
feature_configs=[feature1],
|
||||
)
|
||||
multi_feature_experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
feature_configs=[feature1, feature2],
|
||||
)
|
||||
|
||||
serializer = NimbusExperimentSerializer(NimbusExperiment.objects.all(), many=True)
|
||||
experiments_data = {e["slug"]: e for e in serializer.data.copy()}
|
||||
|
||||
self.assertIn(
|
||||
"feature", experiments_data[single_feature_experiment.slug]["branches"][0]
|
||||
)
|
||||
self.assertIn(
|
||||
"features", experiments_data[single_feature_experiment.slug]["branches"][0]
|
||||
)
|
||||
self.assertIn(
|
||||
"feature", experiments_data[multi_feature_experiment.slug]["branches"][0]
|
||||
)
|
||||
self.assertIn(
|
||||
"features", experiments_data[multi_feature_experiment.slug]["branches"][0]
|
||||
)
|
||||
|
||||
@parameterized.expand(list(NimbusExperiment.Application))
|
||||
def test_serializers_with_missing_feature_value(self, application):
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE,
|
||||
application=application,
|
||||
)
|
||||
experiment.delete_branches()
|
||||
experiment.reference_branch = NimbusBranchFactory(
|
||||
experiment=experiment, feature_values=[]
|
||||
)
|
||||
experiment.save()
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
self.assertEqual(serializer.data["branches"][0]["features"], [])
|
||||
check_schema("experiments/NimbusExperiment", serializer.data)
|
||||
|
||||
def test_serializers_with_empty_feature_value(self):
|
||||
application = NimbusExperiment.Application.DESKTOP
|
||||
feature_config = NimbusFeatureConfigFactory.create(application=application)
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE,
|
||||
application=application,
|
||||
feature_configs=[feature_config],
|
||||
)
|
||||
experiment.delete_branches()
|
||||
experiment.reference_branch = NimbusBranchFactory(
|
||||
experiment=experiment, feature_values=[]
|
||||
)
|
||||
experiment.save()
|
||||
NimbusBranchFeatureValue.objects.create(
|
||||
branch=experiment.reference_branch, feature_config=feature_config, value=""
|
||||
)
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
self.assertEqual(serializer.data["branches"][0]["features"][0]["value"], {})
|
||||
check_schema("experiments/NimbusExperiment", serializer.data)
|
||||
|
||||
def test_serializer_with_branch_invalid_feature_value(self):
|
||||
application = NimbusExperiment.Application.DESKTOP
|
||||
feature_config = NimbusFeatureConfigFactory.create(application=application)
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.CREATED,
|
||||
application=application,
|
||||
feature_configs=[feature_config],
|
||||
)
|
||||
feature_value = experiment.reference_branch.feature_values.get()
|
||||
feature_value.value = "this is not json"
|
||||
feature_value.save()
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
branch_slug = serializer.data["referenceBranch"]
|
||||
branch = next(x for x in serializer.data["branches"] if x["slug"] == branch_slug)
|
||||
self.assertEqual(branch["features"][0]["value"], {})
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
(application, channel, channel_app_id)
|
||||
for application in NimbusExperiment.Application
|
||||
for (channel, channel_app_id) in NimbusExperiment.APPLICATION_CONFIGS[
|
||||
application
|
||||
].channel_app_id.items()
|
||||
]
|
||||
)
|
||||
def test_sets_app_id_name_channel_for_application(
|
||||
self,
|
||||
application,
|
||||
channel,
|
||||
channel_app_id,
|
||||
):
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE,
|
||||
application=application,
|
||||
channel=channel,
|
||||
)
|
||||
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
self.assertEqual(serializer.data["application"], channel_app_id)
|
||||
self.assertEqual(serializer.data["channel"], channel)
|
||||
self.assertEqual(
|
||||
serializer.data["appName"],
|
||||
NimbusExperiment.APPLICATION_CONFIGS[application].app_name,
|
||||
)
|
||||
self.assertEqual(serializer.data["appId"], channel_app_id)
|
||||
check_schema("experiments/NimbusExperiment", serializer.data)
|
||||
|
||||
def test_serializer_outputs_targeting(self):
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
targeting_config_slug=NimbusExperiment.TargetingConfig.FIRST_RUN,
|
||||
channel=NimbusExperiment.Channel.NO_CHANNEL,
|
||||
)
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
self.assertEqual(serializer.data["targeting"], experiment.targeting)
|
||||
check_schema("experiments/NimbusExperiment", serializer.data)
|
||||
|
||||
def test_serializer_outputs_empty_targeting(self):
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE,
|
||||
publish_status=NimbusExperiment.PublishStatus.APPROVED,
|
||||
targeting_config_slug=NimbusExperiment.TargetingConfig.NO_TARGETING,
|
||||
application=NimbusExperiment.Application.FENIX,
|
||||
firefox_min_version=NimbusExperiment.Version.FIREFOX_94,
|
||||
)
|
||||
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
self.assertEqual(serializer.data["targeting"], "true")
|
||||
check_schema("experiments/NimbusExperiment", serializer.data)
|
||||
|
||||
def test_localized_desktop(self):
|
||||
locale_en_us = LocaleFactory.create(code="en-US")
|
||||
locale_en_ca = LocaleFactory.create(code="en-CA")
|
||||
locale_fr = LocaleFactory.create(code="fr")
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
targeting_config_slug=NimbusExperiment.TargetingConfig.NO_TARGETING,
|
||||
channel=NimbusExperiment.Channel.NIGHTLY,
|
||||
primary_outcomes=["foo", "bar", "baz"],
|
||||
secondary_outcomes=["qux", "quux"],
|
||||
is_localized=True,
|
||||
localizations=TEST_LOCALIZATIONS,
|
||||
locales=[locale_en_us, locale_en_ca, locale_fr],
|
||||
)
|
||||
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
|
||||
self.assertIn("localizations", serializer.data)
|
||||
self.assertEqual(serializer.data["localizations"], json.loads(TEST_LOCALIZATIONS))
|
||||
check_schema("experiments/NimbusExperiment", serializer.data)
|
||||
|
||||
def test_multiple_locales(self):
|
||||
locale_en_us = LocaleFactory.create(code="en-US")
|
||||
locale_en_ca = LocaleFactory.create(code="en-CA")
|
||||
locale_fr = LocaleFactory.create(code="fr")
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
targeting_config_slug=NimbusExperiment.TargetingConfig.NO_TARGETING,
|
||||
locales=[locale_en_us, locale_en_ca, locale_fr],
|
||||
)
|
||||
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
|
||||
self.assertIn("locales", serializer.data)
|
||||
self.assertEqual(set(serializer.data["locales"]), {"en-US", "en-CA", "fr"})
|
||||
|
||||
def test_all_locales(self):
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
targeting_config_slug=NimbusExperiment.TargetingConfig.NO_TARGETING,
|
||||
)
|
||||
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
|
||||
self.assertIn("locales", serializer.data)
|
||||
self.assertIsNone(serializer.data["locales"])
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
("invalid json", None),
|
||||
(json.dumps({}), {}),
|
||||
]
|
||||
)
|
||||
def test_localized_localizations_json(self, l10n_json, expected):
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.ENDING_APPROVE_APPROVE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
is_localized=True,
|
||||
localizations=l10n_json,
|
||||
)
|
||||
|
||||
serializer = NimbusExperimentSerializer(experiment)
|
||||
|
||||
self.assertIn("localizations", serializer.data)
|
||||
if expected is None:
|
||||
self.assertIsNone(serializer.data["localizations"])
|
||||
else:
|
||||
self.assertEqual(serializer.data["localizations"], expected)
|
|
@ -0,0 +1,276 @@
|
|||
import json
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from experimenter.experiments.api.v8.serializers import NimbusExperimentSerializer
|
||||
from experimenter.experiments.models import NimbusExperiment
|
||||
from experimenter.experiments.tests.factories import (
|
||||
NimbusExperimentFactory,
|
||||
NimbusFeatureConfigFactory,
|
||||
)
|
||||
|
||||
|
||||
@override_settings(
|
||||
CACHES={
|
||||
"default": {
|
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache",
|
||||
}
|
||||
}
|
||||
)
|
||||
class CachedViewSetTest(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
cache.clear()
|
||||
|
||||
|
||||
class NimbusExperimentFilterMixin:
|
||||
LIFECYCLE = NimbusExperimentFactory.Lifecycles.LIVE_ENROLLING
|
||||
LIST_VIEW = "nimbus-experiment-rest-v8-list"
|
||||
DETAIL_VIEW = "nimbus-experiment-rest-v8-detail"
|
||||
|
||||
def create_experiment_kwargs(self):
|
||||
return {}
|
||||
|
||||
def assert_returned_slugs(self, response, expected_slugs):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
recipes = json.loads(response.content)
|
||||
self.assertEqual(
|
||||
sorted(recipe["slug"] for recipe in recipes),
|
||||
sorted(expected_slugs),
|
||||
)
|
||||
|
||||
def test_filter_by_is_localized(self):
|
||||
NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
slug="experiment",
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
slug="localized_experiment",
|
||||
is_localized=True,
|
||||
localizations=json.dumps(
|
||||
{
|
||||
"en-US": {},
|
||||
"en-CA": {},
|
||||
}
|
||||
),
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse(self.LIST_VIEW),
|
||||
{"is_localized": "True"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
recipes = json.loads(response.content)
|
||||
slugs = [recipe["slug"] for recipe in recipes]
|
||||
|
||||
self.assertEqual(slugs, ["localized_experiment"])
|
||||
|
||||
response = self.client.get(
|
||||
reverse(self.LIST_VIEW),
|
||||
{"is_localized": "False"},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
recipes = json.loads(response.content)
|
||||
slugs = [recipe["slug"] for recipe in recipes]
|
||||
|
||||
self.assertEqual(slugs, ["experiment"])
|
||||
|
||||
def test_filter_by_feature_config(self):
|
||||
features = {
|
||||
slug: NimbusFeatureConfigFactory.create(
|
||||
slug=slug,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
)
|
||||
for slug in ("testFeature", "nimbus-qa-1", "nimbus-qa-2")
|
||||
}
|
||||
|
||||
for feature in features.values():
|
||||
NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
slug=f"{feature.slug}-exp",
|
||||
feature_configs=[feature],
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
|
||||
NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
slug="multi-1",
|
||||
feature_configs=[features["nimbus-qa-1"], features["testFeature"]],
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
|
||||
NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
application=NimbusExperiment.Application.DESKTOP,
|
||||
slug="multi-2",
|
||||
feature_configs=[features["nimbus-qa-2"], features["testFeature"]],
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
|
||||
expected_slugs_by_feature_id = {
|
||||
"nimbus-qa-1": ["nimbus-qa-1-exp", "multi-1"],
|
||||
"nimbus-qa-2": ["nimbus-qa-2-exp", "multi-2"],
|
||||
"testFeature": ["testFeature-exp", "multi-1", "multi-2"],
|
||||
}
|
||||
|
||||
# Test querying for an individual feature ID.
|
||||
for feature_id, expected_slugs in expected_slugs_by_feature_id.items():
|
||||
response = self.client.get(
|
||||
reverse(self.LIST_VIEW),
|
||||
{"feature_config": feature_id},
|
||||
)
|
||||
self.assert_returned_slugs(response, expected_slugs)
|
||||
|
||||
# Test querying for multiple feature IDs
|
||||
response = self.client.get(
|
||||
reverse(self.LIST_VIEW),
|
||||
{"feature_config": ["nimbus-qa-1", "nimbus-qa-2"]},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assert_returned_slugs(
|
||||
response, ["nimbus-qa-1-exp", "nimbus-qa-2-exp", "multi-1", "multi-2"]
|
||||
)
|
||||
|
||||
def test_filter_by_application(self):
|
||||
for application in NimbusExperiment.Application.values:
|
||||
NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
application=application,
|
||||
slug=f"{application}-experiment",
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse(self.LIST_VIEW),
|
||||
{
|
||||
"application": [
|
||||
NimbusExperiment.Application.FOCUS_ANDROID,
|
||||
NimbusExperiment.Application.KLAR_ANDROID,
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
self.assert_returned_slugs(
|
||||
response,
|
||||
[
|
||||
f"{application}-experiment"
|
||||
for application in (
|
||||
NimbusExperiment.Application.FOCUS_ANDROID,
|
||||
NimbusExperiment.Application.KLAR_ANDROID,
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class NimbusExperimentIsFirstRunFilterMixin:
|
||||
def test_filter_by_is_first_run(self):
|
||||
first_run_experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
is_first_run=True,
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
non_first_run_experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
self.LIFECYCLE,
|
||||
is_first_run=False,
|
||||
**self.create_experiment_kwargs(),
|
||||
)
|
||||
|
||||
response = self.client.get(reverse(self.LIST_VIEW), {"is_first_run": "True"})
|
||||
self.assert_returned_slugs(response, [first_run_experiment.slug])
|
||||
|
||||
response = self.client.get(reverse(self.LIST_VIEW), {"is_first_run": "False"})
|
||||
self.assert_returned_slugs(response, [non_first_run_experiment.slug])
|
||||
|
||||
|
||||
class TestNimbusExperimentViewSet(
|
||||
NimbusExperimentFilterMixin, NimbusExperimentIsFirstRunFilterMixin, CachedViewSetTest
|
||||
):
|
||||
maxDiff = None
|
||||
|
||||
def test_list_view_serializes_experiments(self):
|
||||
expected_slugs = []
|
||||
|
||||
for lifecycle in NimbusExperimentFactory.Lifecycles:
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
lifecycle, slug=lifecycle.name
|
||||
)
|
||||
|
||||
if experiment.status not in [
|
||||
NimbusExperiment.Status.DRAFT,
|
||||
]:
|
||||
expected_slugs.append(experiment.slug)
|
||||
|
||||
response = self.client.get(reverse(self.LIST_VIEW))
|
||||
self.assert_returned_slugs(response, expected_slugs)
|
||||
|
||||
def test_get_nimbus_experiment_returns_expected_data(self):
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
NimbusExperimentFactory.Lifecycles.LAUNCH_APPROVE_APPROVE,
|
||||
slug="test-rest-detail",
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse(self.DETAIL_VIEW, kwargs={"slug": experiment.slug}),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
recipes = json.loads(response.content)
|
||||
self.assertEqual(NimbusExperimentSerializer(experiment).data, recipes)
|
||||
|
||||
|
||||
class TestNimbusExperimentDraftViewSet(
|
||||
NimbusExperimentFilterMixin, NimbusExperimentIsFirstRunFilterMixin, CachedViewSetTest
|
||||
):
|
||||
maxDiff = None
|
||||
|
||||
LIST_VIEW = "nimbus-experiment-rest-v8-draft-list"
|
||||
DETAIL_VIEW = "nimbus-experiment-rest-v8-draft-detail"
|
||||
LIFECYCLE = NimbusExperimentFactory.Lifecycles.CREATED
|
||||
|
||||
def test_detail_view_serializes_draft_experiments(self):
|
||||
draft_slugs = []
|
||||
non_draft_slugs = []
|
||||
|
||||
for lifecycle in NimbusExperimentFactory.Lifecycles:
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
lifecycle,
|
||||
slug=lifecycle.name,
|
||||
)
|
||||
|
||||
if experiment.status == NimbusExperiment.Status.DRAFT:
|
||||
draft_slugs.append(experiment.slug)
|
||||
else:
|
||||
non_draft_slugs.append(experiment.slug)
|
||||
|
||||
for slug in draft_slugs:
|
||||
response = self.client.get(reverse(self.DETAIL_VIEW, kwargs={"slug": slug}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
for slug in non_draft_slugs:
|
||||
response = self.client.get(reverse(self.DETAIL_VIEW, kwargs={"slug": slug}))
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_list_view_serializes_draft_experiments(self):
|
||||
expected_slugs = []
|
||||
|
||||
for lifecycle in NimbusExperimentFactory.Lifecycles:
|
||||
experiment = NimbusExperimentFactory.create_with_lifecycle(
|
||||
lifecycle,
|
||||
slug=lifecycle.name,
|
||||
)
|
||||
|
||||
if experiment.status == NimbusExperiment.Status.DRAFT:
|
||||
expected_slugs.append(experiment.slug)
|
||||
|
||||
response = self.client.get(reverse(self.LIST_VIEW))
|
||||
self.assert_returned_slugs(response, expected_slugs)
|
|
@ -203,6 +203,10 @@ OPENIDC_AUTH_WHITELIST = (
|
|||
"nimbus-experiment-rest-v6-draft-detail",
|
||||
"nimbus-experiment-rest-v7-list",
|
||||
"nimbus-experiment-rest-v7-detail",
|
||||
"nimbus-experiment-rest-v8-list",
|
||||
"nimbus-experiment-rest-v8-detail",
|
||||
"nimbus-experiment-rest-v8-draft-list",
|
||||
"nimbus-experiment-rest-v8-draft-detail",
|
||||
)
|
||||
|
||||
# Internationalization
|
||||
|
@ -367,7 +371,7 @@ CACHES = {
|
|||
"TIMEOUT": None,
|
||||
},
|
||||
}
|
||||
V6_API_CACHE_DURATION = 60 * 60
|
||||
API_CACHE_DURATION = 60 * 60
|
||||
SIZING_DATA_KEY = "population_sizing"
|
||||
|
||||
# Celery
|
||||
|
|
|
@ -24,6 +24,7 @@ urlpatterns = [
|
|||
re_path(r"^api/v5/", include("experimenter.experiments.api.v5.urls")),
|
||||
re_path(r"^api/v6/", include("experimenter.experiments.api.v6.urls")),
|
||||
re_path(r"^api/v7/", include("experimenter.experiments.api.v7.urls")),
|
||||
re_path(r"^api/v8/", include("experimenter.experiments.api.v8.urls")),
|
||||
re_path(r"^admin/", admin.site.urls),
|
||||
re_path(r"^experiments/", include("experimenter.legacy.legacy_experiments.urls")),
|
||||
re_path(r"^nimbus_new/", include("experimenter.nimbus_ui_new.urls")),
|
||||
|
|
Загрузка…
Ссылка в новой задаче