Remove monitoring app (#784)

* Remove monitoring app

* remove monitoring cicd
This commit is contained in:
Lukasz Ostrowski 2023-07-19 10:21:39 +02:00 committed by GitHub
parent 70cb741f88
commit 44333a6784
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 0 additions and 6881 deletions

View file

@ -4,7 +4,6 @@
"saleor-app-emails-and-messages": patch "saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch "saleor-app-data-importer": patch
"saleor-app-products-feed": patch "saleor-app-products-feed": patch
"saleor-app-monitoring": patch
"@saleor/apps-shared": patch "@saleor/apps-shared": patch
"saleor-app-invoices": patch "saleor-app-invoices": patch
"saleor-app-klaviyo": patch "saleor-app-klaviyo": patch

View file

@ -2,7 +2,6 @@
"saleor-app-emails-and-messages": patch "saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch "saleor-app-data-importer": patch
"saleor-app-products-feed": patch "saleor-app-products-feed": patch
"saleor-app-monitoring": patch
"@saleor/apps-shared": patch "@saleor/apps-shared": patch
"saleor-app-invoices": patch "saleor-app-invoices": patch
"saleor-app-klaviyo": patch "saleor-app-klaviyo": patch

View file

@ -88,27 +88,6 @@ updates:
interval: weekly interval: weekly
commit-message: commit-message:
prefix: "[skip ci]" prefix: "[skip ci]"
- package-ecosystem: "npm"
directory: apps/monitoring
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "pip"
directory: apps/monitoring/backend
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "docker"
directory: apps/monitoring/backend
open-pull-requests-limit: 1
schedule:
interval: weekly
commit-message:
prefix: "[skip ci]"
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: apps/products-feed directory: apps/products-feed
open-pull-requests-limit: 1 open-pull-requests-limit: 1

1
.github/labeler.yml vendored
View file

@ -1,7 +1,6 @@
"App: Data Importer": "apps/data-importer/**/*" "App: Data Importer": "apps/data-importer/**/*"
"App: Invoices": "apps/invoices/**/*" "App: Invoices": "apps/invoices/**/*"
"App: Klaviyo": "apps/klaviyo/**/*" "App: Klaviyo": "apps/klaviyo/**/*"
"App: Monitoring": "apps/monitoring/**/*"
"App: Product Feed": "apps/products-feed/**/*" "App: Product Feed": "apps/products-feed/**/*"
"App: Search": "apps/search/**/*" "App: Search": "apps/search/**/*"
"App: Slack": "apps/slack/**/*" "App: Slack": "apps/slack/**/*"

View file

@ -1,81 +0,0 @@
name: Publish image
on:
push:
branches:
- main
paths:
- apps/monitoring/backend/**
jobs:
publish:
runs-on: ubuntu-22.04
env:
AWS_REGION: eu-west-1
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
with:
platforms: all
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
with:
install: true
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_APPS_STAGING_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_APPS_STAGING_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- id: ecr-login
name: Login to Amazon ECR
uses: aws-actions/amazon-ecr-login@v1
with:
registries: ${{ secrets.AWS_ECR_ACCOUNT }}
- name: Evaluate image tags
run: |
IMAGE_REPOSITORY=${{ steps.ecr-login.outputs.registry }}/${{ secrets.ECR_REPOSITORY }}
BRANCH_IMAGE_TAG=${{ github.ref_name }}
UNIQUE_IMAGE_TAG=${BRANCH_IMAGE_TAG}-$(git rev-parse --short HEAD)
IMAGE_TAGS=${IMAGE_REPOSITORY}:${BRANCH_IMAGE_TAG},${IMAGE_REPOSITORY}:${UNIQUE_IMAGE_TAG}
echo "UNIQUE_IMAGE_TAG=${UNIQUE_IMAGE_TAG}" >> $GITHUB_ENV
echo "IMAGE_TAGS=${IMAGE_TAGS}" >> $GITHUB_ENV
- name: Build and push
timeout-minutes: 20
uses: docker/build-push-action@v4
with:
context: ./apps/monitoring/backend
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ env.IMAGE_TAGS }}
target: prod
cache-from: type=gha,scope=buildkit-master
cache-to: type=gha,scope=buildkit-master
# - name: Configure GitHub credentials
# run: |
# GITHUB_TOKEN=$( \
# curl --request GET --url ${{ secrets.VAULT_URL}} --header "Authorization: JWT ${{ secrets.VAULT_JWT }}" | jq -r .token \
# )
# echo "GITHUB_TOKEN=${GITHUB_TOKEN}" >> $GITHUB_ENV
# - name: Trigger Helm deployment
# run: |
# gh api /repos/saleor/saleor-cloud-deployments/dispatches \
# --input - <<< '{
# "event_type": "deploy-app-monitoring-staging",
# "client_payload": {
# "image_tag": "${{ env.UNIQUE_IMAGE_TAG }}"
# }
# }'

View file

@ -1,33 +0,0 @@
name: "App: Monitoring backend tests"
on:
pull_request:
paths:
- "apps/monitoring/backend/**"
jobs:
unit_test:
name: Unit tests
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/monitoring/backend
steps:
- uses: actions/checkout@v3
- name: Install poetry
run: pipx install poetry
- name: Setup python
uses: actions/setup-python@v4
with:
python-version: "3.10"
cache: poetry
- name: Install dependencies
run: poetry install
- name: Run unit tests
run: poetry run pytest
- name: Run black
run: poetry run black .
- name: Run ruff
run: poetry run ruff .
- name: Run mypy
run: poetry run mypy .

View file

@ -1 +0,0 @@
MONITORING_APP_API_URL=

View file

@ -1,4 +0,0 @@
{
"root": true,
"extends": ["saleor"]
}

View file

@ -1,125 +0,0 @@
# Environments
.venv
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Ruff
.ruff_cache
# Backend
.fileApl.json

View file

@ -1,19 +0,0 @@
schema: backend/monitoring/schema.graphql
documents: [src/**/*.tsx]
extensions:
codegen:
overwrite: true
generates:
generated/graphql.ts:
config:
dedupeFragments: true
plugins:
- typescript
- typescript-operations
- typescript-urql:
documentVariablePrefix: "Untyped"
fragmentVariablePrefix: "Untyped"
- typed-document-node
generated/schema.graphql:
plugins:
- schema-ast

View file

@ -1,6 +0,0 @@
.next
saleor/api.tsx
pnpm-lock.yaml
graphql/schema.graphql
generated
backend

View file

@ -1,4 +0,0 @@
{
"singleQuote": false,
"printWidth": 100
}

View file

@ -1,189 +0,0 @@
# saleor-app-monitoring
## 1.1.0
### Minor Changes
- 47102ba: Added additional ENV variables (see each app's .env.example), that can overwrite app base URL. This change allows easy apps development using Docker
### Patch Changes
- 2d77bca: Updated Next.js to 13.4.8
- 6299e06: Update @saleor/app-sdk to 0.41.0
- Updated dependencies [2d77bca]
- Updated dependencies [6299e06]
- @saleor/apps-shared@1.7.3
- @saleor/apps-ui@1.1.3
- @saleor/react-hook-form-macaw@0.2.0
## 1.0.0
### Major Changes
- 3bd7e3f: Updated App's UI to the new Macaw. Simplified the view and removed unnecessary not-implemented providers.
### Minor Changes
- 1dead1e: Included dedicated logo and attached it to App's manifest. From Saleor 3.15 the logo will be visible in the Dashboard during and after installation.
### Patch Changes
- 860bac4: Updated @saleor/app-sdk to 0.40.1
- a1ad70e: Updated configuration and dependencies of GraphQL client - urql.
All applications use now unified config for creating the client. Also unused related packages has been removed.
- cb6ee29: Updated dependencies
- Updated dependencies [f96563f]
- Updated dependencies [f96563f]
- Updated dependencies [860bac4]
- Updated dependencies [a1ad70e]
- Updated dependencies [cb6ee29]
- Updated dependencies [a1ad70e]
- @saleor/react-hook-form-macaw@0.2.0
- @saleor/apps-ui@1.1.2
- @saleor/apps-shared@1.7.2
## 0.6.6
### Patch Changes
- a8834a1: Unified graphql version to 16.6
- a8834a1: Unified graphql codegen packages
- a8834a1: Removed unnecessary duplicated dependencies from apps and moved them to shared and root (types, eslint rules)
- a8834a1: Updated dev dependencies - Typescript, Eslint and Turborepo
- Updated dependencies [a8834a1]
- Updated dependencies [a8834a1]
- Updated dependencies [a8834a1]
- Updated dependencies [a8834a1]
- @saleor/apps-shared@1.7.1
## 0.6.5
### Patch Changes
- 0c2fc65: Update dev dependencies - Vite and Vitest. These changes will not affect runtime Apps, but can affect tests and builds
- Updated dependencies [0c2fc65]
- Updated dependencies [b75a664]
- @saleor/apps-shared@1.7.0
## 0.6.4
### Patch Changes
- 6e69f4f: Update app-sdk to 0.39.1
- Updated dependencies [6e69f4f]
- @saleor/apps-shared@1.6.1
## 0.6.3
### Patch Changes
- Updated dependencies [23b5c70]
- @saleor/apps-shared@1.6.0
## 0.6.2
### Patch Changes
- c406318: Updated dep @saleor/app-sdk to 0.38.0
- Updated dependencies [c406318]
- @saleor/apps-shared@1.5.1
## 0.6.1
### Patch Changes
- 8b22b1c: Restored Pino logger packages to each app, to fix failing logs in development. Also updated .env.example to contain up to date APP_LOG_LEVEL variable
## 0.6.0
### Minor Changes
- 830cfe9: Changed APP_DEBUG env to APP_LOG_LEVEL
### Patch Changes
- Updated dependencies [830cfe9]
- @saleor/apps-shared@1.5.0
## 0.5.0
### Minor Changes
- 57f6d41: Updated Manifest to contain up to date support, privacy, homepage and author fields
### Patch Changes
- 2c0df91: Added lint:fix script, so `eslint --fix` can be run deliberately
- e167e72: Update next.js to 13.3.0
- 74174c4: Updated @saleor/app-sdk to 0.37.3
- 2e51890: Update next.js to 13.3.0
- 2e51890: Update @saleor/app-sdk to 0.37.2
- 2e51890: Use useDashboardNotification hook from shared package, instead of direct AppBridge usage
- Updated dependencies [2c0df91]
- Updated dependencies [e167e72]
- Updated dependencies [74174c4]
- Updated dependencies [2e51890]
- Updated dependencies [2e51890]
- Updated dependencies [2e51890]
- @saleor/apps-shared@1.4.0
## 0.4.1
### Patch Changes
- eca52ad: Replace "export default" with named exports
- @saleor/apps-shared@1.3.0
## 0.4.0
### Minor Changes
- 7cb3b89: Added "author" field to the Manifest, set it to Saleor Commerce, so Dashboard can display it too
### Patch Changes
- 7cb3b89: Replace apps to avoid AppPermission (use Permission for client permissions) and authData.domain (use saleorApiUrl)
- 7cb3b89: Updated @saleor/app-sdk to 0.37.1
## 0.3.3
### Patch Changes
- e93a4dc: Updated GraphQL Code Generator package
## 0.3.2
### Patch Changes
- dca82bb: Update app-sdk to pre-0.34.0. Update Async Webhooks to use new API
## 0.3.1
### Patch Changes
- 2755ed2: Added extra padding on top of the app so it has some space between content and dashboard header
## 0.3.0
### Minor Changes
- 2d23480: Remove TitleBar component from apps, because it is moved to Dashboard, outside of iframe context
### Patch Changes
- Updated dependencies [2d23480]
- @saleor/apps-shared@1.3.0
## 0.2.0
### Minor Changes
- 289b42f: Breaking change for app maintainers: VercelAPL can no longer be set for the app since it's deprecated and will be removed in app-sdk 0.30.0. As a replacement, we recommend using Upstash APL or implementing your own.
Read more about APLs: https://github.com/saleor/saleor-app-sdk/blob/main/docs/apl.md
## 0.1.1
### Patch Changes
- Updated dependencies [5fc88ed]
- @saleor/apps-shared@1.2.0

View file

@ -1,78 +0,0 @@
# Saleor Monitoring app
NOTE: This is Alpha version of the app.❗️
## Local development
### Start Monitoring backend
Run:
```shell
docker-compose up
```
It is beneficial to run this command in a separate terminal tab to observe backend logs easily.
By default, backend will run at `localhost:5001` with:
- Manifest at `/manifest`
- Graphql Playground at `/graphql`
- OpenApi viewer at `/docs`
### Develop frontend:
Installing dependencies with:
```shell
pnpm i
```
Running dev server
```shell
pnpm dev
```
The frontend app will run at `localhost:3000`.
By default, it acts as a proxy and redirects all unhandled requests to the backend (configured by `MONITORING_APP_API_URL` env).
This way, all frontend and backend endpoints are accessible at `http://localhost:3000`
### Test with Saleor
Expose `http://localhost:3000` using a tunnel and use `https://your.tunnel/manifest` manifest URL to install `Monitoring` app
### Graphql Playground
To use Graphql Playground, `Monitoring` app needs to be installed in Saleor, and HTTP headers must be set:
```json
{
"authorization-bearer": "token",
"saleor-api-url": "https://my-env.saleor.cloud/graphql/"
}
```
### Testing DataDog integration
Set `MOCK_DATADOG_CLIENT` env to `True`
Use these credentials sets to test DataDog integration:
Working credentials:
```json
{
"site": "US1",
"apiKey": "156e22d50c4e8b6816e1fd4794d3fd8c"
}
```
Credentials that validate but generate an error while sending events
```json
{
"site": "EU1",
"apiKey": "156e22d50c4e8b6816e1fd4794d3fd8c"
}
```

View file

@ -1 +0,0 @@
**/.fileApl.json

View file

@ -1,28 +0,0 @@
FROM python:3.10 as build
RUN pip install poetry'>=1.3.2,<1.4.0'
WORKDIR /app
COPY pyproject.toml poetry.lock /app/
RUN POETRY_VIRTUALENVS_CREATE=false poetry install --no-cache --only main
FROM python:3.10-slim as prod
ENV PYTHONUNBUFFERED 1
COPY --from=build /usr/local/bin/ /usr/local/bin/
COPY --from=build /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
WORKDIR /app
COPY monitoring/ /app/monitoring/
EXPOSE 80
CMD ["uvicorn", "monitoring.app:app", "--host", "0.0.0.0", "--port", "80", "--no-access-log", "--forwarded-allow-ips", "*"]
FROM prod as dev
COPY pyproject.toml poetry.lock /app/
RUN POETRY_VIRTUALENVS_CREATE=false poetry install --no-cache --only dev

View file

@ -1 +0,0 @@
__version__ = "0.1.0"

View file

@ -1,15 +0,0 @@
import uvicorn
def main():
uvicorn.run(
"monitoring.app:app",
host="0.0.0.0",
port=5001,
reload=True,
forwarded_allow_ips="*",
)
if __name__ == "__main__":
main()

View file

@ -1,93 +0,0 @@
from pathlib import Path
from ariadne import (
MutationType,
ObjectType,
QueryType,
convert_kwargs_to_snake_case,
load_schema_from_path,
make_executable_schema,
snake_case_fallback_resolvers,
)
from ariadne.asgi import GraphQL
from graphql import GraphQLResolveInfo
from .deps import ApiDependencies
from .schema import DatadogConfig, DatadogCredentials
from .settings import settings
base_dir = Path(__file__).resolve().parent
type_defs = load_schema_from_path(str(base_dir / "schema.graphql"))
query = QueryType()
datadog_config = ObjectType("DatadogConfig")
mutation = MutationType()
def get_api_context(info: GraphQLResolveInfo) -> ApiDependencies:
return info.context["request"].state.api_context
@datadog_config.field("credentials")
def resolve_datadog_credentials(datadog: DatadogConfig, *_):
return {
"site": datadog.credentials.site,
"api_key_last_4": datadog.credentials.api_key[-4:],
}
@query.field("integrations")
async def resolve_integrations(_, info):
context = get_api_context(info)
metadata = await context.manager.get_metadata()
return metadata
@mutation.field("updateDatadogConfig")
@convert_kwargs_to_snake_case
async def resolve_update_datadog(_, info, input):
context = get_api_context(info)
metadata = await context.manager.get_metadata()
if creds := input.get("credentials", None):
creds = DatadogCredentials(site=creds["site"], api_key=creds["api_key"])
if not await context.datadog_client.validate_credentials(creds):
return {
"errors": [
{
"field": "credentials",
"message": f"Invalid datadog apiKey for site {creds.site}",
}
]
}
# Create new configuration
if metadata.datadog is None:
if creds is None:
return {"errors": [{"message": "No DataDog config to update"}]}
metadata.datadog = DatadogConfig(credentials=creds)
if "active" in input:
metadata.datadog.active = input["active"]
if creds:
metadata.datadog.credentials = creds
metadata.datadog.error = None
await context.manager.save_private_metadata(metadata)
return {"datadog": metadata.datadog, "errors": []}
@mutation.field("deleteDatadogConfig")
async def resolve_delete_datadog(_, info):
context = get_api_context(info)
metadata = await context.manager.get_metadata()
if metadata.datadog is None:
return {"errors": [{"message": "No DataDog config to delete"}]}
await context.manager.delete_private_metadata("datadog")
return {"datadog": metadata.datadog, "errors": []}
schema = make_executable_schema(
type_defs, query, datadog_config, mutation, snake_case_fallback_resolvers
)
graphQL = GraphQL(schema, debug=settings.debug)
graphql_app = graphQL.http_handler

View file

@ -1,12 +0,0 @@
from .common import AplEntity, AplError, AplKeyError
from .wrapper import AplClient
apl_client = AplClient()
__all__ = [
"apl_client",
"AplClient",
"AplEntity",
"AplError",
"AplKeyError",
]

View file

@ -1,6 +0,0 @@
from .base import AplBackend
from .file import FileAplBackend
from .mem import MemAplBackend
from .rest import RestAplBackend
__all__ = ["AplBackend", "MemAplBackend", "FileAplBackend", "RestAplBackend"]

View file

@ -1,25 +0,0 @@
from typing import AsyncGenerator
from ..common import AplEntity
class AplBackend:
async def get(self, key: str) -> AplEntity:
raise NotImplementedError(
"subclasses of BaseAPLClient must provide a get() method"
)
async def set(self, key: str, value: AplEntity):
raise NotImplementedError(
"subclasses of BaseAPLClient must provide a add() method"
)
async def delete(self, key: str):
raise NotImplementedError(
"subclasses of BaseAPLClient must provide a delete() method"
)
def get_all(self, page_size: int) -> AsyncGenerator[tuple[str, AplEntity], None]:
raise NotImplementedError(
"subclasses of BaseAPLClient must provide a delete() method"
)

View file

@ -1,31 +0,0 @@
from json import dumps
from pathlib import Path
from fastapi.encoders import jsonable_encoder
from pydantic import parse_file_as
from ..common import AplEntity
from .mem import MemAplBackend
class FileAplBackend(MemAplBackend):
def _load_file(self):
self._apl = parse_file_as(dict[str, AplEntity], self.path)
def _save_file(self):
data = dumps(jsonable_encoder(self._apl), indent=2)
self.path.write_text(data)
def __init__(self, path: Path):
super().__init__()
self.path = path
if self.path.exists():
self._load_file()
async def set(self, *args, **kwargs):
await super().set(*args, **kwargs)
self._save_file()
async def delete(self, *args, **kwargs):
await super().delete(*args, **kwargs)
self._save_file()

View file

@ -1,26 +0,0 @@
from ..common import AplEntity, AplKeyError
from .base import AplBackend
class MemAplBackend(AplBackend):
def __init__(self):
self._apl: dict[str, AplEntity] = {}
async def get(self, key: str):
try:
return self._apl[key]
except KeyError as err:
raise AplKeyError(f"Key: {key} not found in MemApl") from err
async def set(self, key: str, value: AplEntity):
self._apl[key] = value
async def delete(self, key: str):
try:
del self._apl[key]
except KeyError as err:
raise AplKeyError(f"Key: {key} not found in MemApl") from err
async def get_all(self, page_size: int):
for key, val in self._apl.items():
yield key, val

View file

@ -1,83 +0,0 @@
import json
from base64 import urlsafe_b64encode
from contextlib import asynccontextmanager
from urllib.parse import urlparse
import httpx
from pydantic import BaseModel
from ..common import AplEntity, AplError, AplKeyError
from .base import AplBackend
class AplPage(BaseModel):
count: int
next: str | None
previous: str | None
results: list[AplEntity]
class RestAplBackend(AplBackend):
def __init__(self, apl_url: str, token: str):
self.apl_url = apl_url
self.token = token
@property
def headers(self):
return {"Authorization": f"Bearer {self.token}"}
@staticmethod
def map_apl_entity(value: AplEntity):
return {
"saleor_app_id": value.app_id,
"saleor_api_url": value.saleor_api_url,
"jwks": json.dumps(value.jwks),
"domain": urlparse(value.saleor_api_url).netloc,
"token": value.app_token,
}
@staticmethod
def b64_encode(key: str):
return urlsafe_b64encode(key.encode()).decode()
@asynccontextmanager
async def _client(self):
headers = {"Authorization": f"Bearer {self.token}"}
async with httpx.AsyncClient(base_url=self.apl_url, headers=headers) as client:
try:
yield client
except httpx.HTTPError as exc:
raise AplError("RestApl error") from exc
async def set(self, key: str, value: AplEntity):
async with self._client() as client:
resp = await client.post("/api/v1/apl", json=self.map_apl_entity(value))
resp.raise_for_status()
async def get(self, key: str):
async with self._client() as client:
resp = await client.get(f"/api/v1/apl/{self.b64_encode(key)}")
if resp.status_code == 404:
raise AplKeyError(f"Key: {key} not found in RestApl")
resp.raise_for_status()
return AplEntity.parse_raw(resp.content)
async def delete(self, key: str):
async with self._client() as client:
resp = await client.delete(f"/api/v1/apl/{self.b64_encode(key)}")
if resp.status_code == 404:
raise AplKeyError(f"Key: {key} not found in RestApl")
resp.raise_for_status()
async def get_all(self, page_size: int):
async with self._client() as client:
offset, count = 0, 1
while offset < count:
params = {"limit": page_size, "offset": offset}
resp = await client.get("/api/v1/apl", params=params)
resp.raise_for_status()
page = AplPage.parse_raw(resp.content)
count = page.count
offset += page_size
for elem in page.results:
yield elem.saleor_api_url, elem

View file

@ -1,32 +0,0 @@
import json
from typing import Any
from pydantic import AnyHttpUrl, BaseModel, Field, validator
class AplEntity(BaseModel):
saleor_api_url: AnyHttpUrl
app_id: str = Field(..., alias="saleor_app_id")
app_token: str = Field(..., alias="token")
jwks: dict[str, Any]
class Config:
allow_population_by_field_name = True
@validator("jwks", pre=True)
def parse_json(cls, v):
if isinstance(v, str):
return json.loads(v)
return v
class AplError(Exception):
pass
class AplKeyError(AplError):
pass
class NotConfiguredError(AplError):
"""If apl client was not configured"""

View file

@ -1,135 +0,0 @@
interactions:
- request:
body: '{"saleor_app_id": "uzfbqpkchx", "saleor_api_url": "https://sbnpsagopi.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "sbnpsagopi.saleor.cloud",
"token": "lbnetlmnjviofvjvleoyhotmbsaheg"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 16:51:49 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: DELETE
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9zYm5wc2Fnb3BpLnNhbGVvci5jbG91ZC9ncmFwaHFs
response:
content: ''
headers:
Connection:
- keep-alive
Content-Length:
- '0'
Date:
- Sun, 19 Feb 2023 16:51:49 GMT
allow:
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 204
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9zYm5wc2Fnb3BpLnNhbGVvci5jbG91ZC9ncmFwaHFs
response:
content: '{"detail":"Not found."}'
headers:
Connection:
- keep-alive
Content-Length:
- '23'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 16:51:50 GMT
allow:
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 404
version: 1

View file

@ -1,44 +0,0 @@
interactions:
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: DELETE
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9ub24uZXhpc3RpbmcuZG9tYWlu
response:
content: '{"detail":"Not found."}'
headers:
Connection:
- keep-alive
Content-Length:
- '23'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 16:59:52 GMT
allow:
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 404
version: 1

View file

@ -1,98 +0,0 @@
interactions:
- request:
body: '{"saleor_app_id": "uzfbqpkchx", "saleor_api_url": "https://sbnpsagopi.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "sbnpsagopi.saleor.cloud",
"token": "lbnetlmnjviofvjvleoyhotmbsaheg"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 16:48:47 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9zYm5wc2Fnb3BpLnNhbGVvci5jbG91ZC9ncmFwaHFs
response:
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 16:48:48 GMT
allow:
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 200
version: 1

View file

@ -1,742 +0,0 @@
interactions:
- request:
body: '{"saleor_app_id": "uzfbqpkchx", "saleor_api_url": "https://sbnpsagopi.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "sbnpsagopi.saleor.cloud",
"token": "lbnetlmnjviofvjvleoyhotmbsaheg"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:37 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "fzglnnohvi", "saleor_api_url": "https://msltedyydo.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"msfmpvhxqiqptpcfpvomenrslrmpwf\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "msltedyydo.saleor.cloud",
"token": "etqfdiojfvufyfpylknharwbkkhcio"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://msltedyydo.saleor.cloud/graphql","token":"etqfdiojfvufyfpylknharwbkkhcio","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"msfmpvhxqiqptpcfpvomenrslrmpwf\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fzglnnohvi","domain":"msltedyydo.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:38 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "lcedtozpeo", "saleor_api_url": "https://rodnbaorcb.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"djnhwjcbtrydjfvpeltmzglfcsgngd\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "rodnbaorcb.saleor.cloud",
"token": "nyqokxizwonkuqqexhktbvgussupaz"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://rodnbaorcb.saleor.cloud/graphql","token":"nyqokxizwonkuqqexhktbvgussupaz","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"djnhwjcbtrydjfvpeltmzglfcsgngd\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"lcedtozpeo","domain":"rodnbaorcb.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:38 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "vxrgitlduz", "saleor_api_url": "https://vjoxywqxno.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"pqimouklxipopspcwrxlzxmzftnxsc\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "vjoxywqxno.saleor.cloud",
"token": "uagntnmzugcynzcblilonyguxjowjq"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://vjoxywqxno.saleor.cloud/graphql","token":"uagntnmzugcynzcblilonyguxjowjq","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"pqimouklxipopspcwrxlzxmzftnxsc\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"vxrgitlduz","domain":"vjoxywqxno.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:39 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "mtljtfhfjd", "saleor_api_url": "https://txjaskukcx.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"bbslmktufyfdtzlfaxrqzlcmoslbeo\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "txjaskukcx.saleor.cloud",
"token": "rzbhsxcngssozsamgksgqzsyykbvws"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://txjaskukcx.saleor.cloud/graphql","token":"rzbhsxcngssozsamgksgqzsyykbvws","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"bbslmktufyfdtzlfaxrqzlcmoslbeo\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"mtljtfhfjd","domain":"txjaskukcx.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:39 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "krqomgdnue", "saleor_api_url": "https://wptoxotfln.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"nclygpffmzyoykbffswnequwmdmdhd\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "wptoxotfln.saleor.cloud",
"token": "bfuugsnzlrmbtbfjhbsfduslfjbmty"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://wptoxotfln.saleor.cloud/graphql","token":"bfuugsnzlrmbtbfjhbsfduslfjbmty","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"nclygpffmzyoykbffswnequwmdmdhd\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"krqomgdnue","domain":"wptoxotfln.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:39 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "qtpthrjavf", "saleor_api_url": "https://nxujxjrudl.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"fxblkupzfagejghhmtrstnryaeadab\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "nxujxjrudl.saleor.cloud",
"token": "vmwhdquypdsmsgjlcgtrqacmzbaunk"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://nxujxjrudl.saleor.cloud/graphql","token":"vmwhdquypdsmsgjlcgtrqacmzbaunk","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"fxblkupzfagejghhmtrstnryaeadab\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"qtpthrjavf","domain":"nxujxjrudl.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:40 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "nxvftnbrfb", "saleor_api_url": "https://vyyfdwmcav.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ydrdjwtzkzasiuyqnqewbubtbugrag\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "vyyfdwmcav.saleor.cloud",
"token": "wmsookfbmuopwwlvceiifcoaeicqje"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://vyyfdwmcav.saleor.cloud/graphql","token":"wmsookfbmuopwwlvceiifcoaeicqje","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ydrdjwtzkzasiuyqnqewbubtbugrag\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"nxvftnbrfb","domain":"vyyfdwmcav.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:40 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "dpjoofueam", "saleor_api_url": "https://ejwnealqvw.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ukxkebobvkqqbmtyzcwxbebalnhvps\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "ejwnealqvw.saleor.cloud",
"token": "teovwyfkqvpebpsmfpwigpqlusyrme"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://ejwnealqvw.saleor.cloud/graphql","token":"teovwyfkqvpebpsmfpwigpqlusyrme","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ukxkebobvkqqbmtyzcwxbebalnhvps\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"dpjoofueam","domain":"ejwnealqvw.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:41 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: '{"saleor_app_id": "fxeyumrwrg", "saleor_api_url": "https://vxcdclzonj.saleor.cloud/graphql",
"jwks": "{\"keys\": [{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"aofvflmughkjsiebpnlkakvjqywsbn\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}", "domain": "vxcdclzonj.saleor.cloud",
"token": "vxhxemgqtpvvmzgcrxxetsrecvmocg"}'
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
content-length:
- '337'
content-type:
- application/json
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: POST
uri: https://apl.example.com/api/v1/apl
response:
content: '{"saleor_api_url":"https://vxcdclzonj.saleor.cloud/graphql","token":"vxhxemgqtpvvmzgcrxxetsrecvmocg","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"aofvflmughkjsiebpnlkakvjqywsbn\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fxeyumrwrg","domain":"vxcdclzonj.saleor.cloud"}'
headers:
Connection:
- keep-alive
Content-Length:
- '328'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:41 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 201
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl?limit=2&offset=0
response:
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=2","previous":null,"results":[{"saleor_api_url":"https://sbnpsagopi.saleor.cloud/graphql","token":"lbnetlmnjviofvjvleoyhotmbsaheg","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"jrlyhkvronpcuskqfhnhbbpjtvcrce\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"uzfbqpkchx","domain":"sbnpsagopi.saleor.cloud"},{"saleor_api_url":"https://msltedyydo.saleor.cloud/graphql","token":"etqfdiojfvufyfpylknharwbkkhcio","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"msfmpvhxqiqptpcfpvomenrslrmpwf\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fzglnnohvi","domain":"msltedyydo.saleor.cloud"}]}'
headers:
Connection:
- keep-alive
Content-Length:
- '779'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:41 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 200
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl?limit=2&offset=2
response:
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=4","previous":"http://apl.example.com/api/v1/apl?limit=2","results":[{"saleor_api_url":"https://rodnbaorcb.saleor.cloud/graphql","token":"nyqokxizwonkuqqexhktbvgussupaz","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"djnhwjcbtrydjfvpeltmzglfcsgngd\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"lcedtozpeo","domain":"rodnbaorcb.saleor.cloud"},{"saleor_api_url":"https://vjoxywqxno.saleor.cloud/graphql","token":"uagntnmzugcynzcblilonyguxjowjq","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"pqimouklxipopspcwrxlzxmzftnxsc\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"vxrgitlduz","domain":"vjoxywqxno.saleor.cloud"}]}'
headers:
Connection:
- keep-alive
Content-Length:
- '839'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:42 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 200
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl?limit=2&offset=4
response:
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=6","previous":"http://apl.example.com/api/v1/apl?limit=2&offset=2","results":[{"saleor_api_url":"https://txjaskukcx.saleor.cloud/graphql","token":"rzbhsxcngssozsamgksgqzsyykbvws","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"bbslmktufyfdtzlfaxrqzlcmoslbeo\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"mtljtfhfjd","domain":"txjaskukcx.saleor.cloud"},{"saleor_api_url":"https://wptoxotfln.saleor.cloud/graphql","token":"bfuugsnzlrmbtbfjhbsfduslfjbmty","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"nclygpffmzyoykbffswnequwmdmdhd\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"krqomgdnue","domain":"wptoxotfln.saleor.cloud"}]}'
headers:
Connection:
- keep-alive
Content-Length:
- '848'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:42 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 200
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl?limit=2&offset=6
response:
content: '{"count":10,"next":"http://apl.example.com/api/v1/apl?limit=2&offset=8","previous":"http://apl.example.com/api/v1/apl?limit=2&offset=4","results":[{"saleor_api_url":"https://nxujxjrudl.saleor.cloud/graphql","token":"vmwhdquypdsmsgjlcgtrqacmzbaunk","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"fxblkupzfagejghhmtrstnryaeadab\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"qtpthrjavf","domain":"nxujxjrudl.saleor.cloud"},{"saleor_api_url":"https://vyyfdwmcav.saleor.cloud/graphql","token":"wmsookfbmuopwwlvceiifcoaeicqje","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ydrdjwtzkzasiuyqnqewbubtbugrag\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"nxvftnbrfb","domain":"vyyfdwmcav.saleor.cloud"}]}'
headers:
Connection:
- keep-alive
Content-Length:
- '848'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:42 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 200
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl?limit=2&offset=8
response:
content: '{"count":10,"next":null,"previous":"http://apl.example.com/api/v1/apl?limit=2&offset=6","results":[{"saleor_api_url":"https://ejwnealqvw.saleor.cloud/graphql","token":"teovwyfkqvpebpsmfpwigpqlusyrme","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"ukxkebobvkqqbmtyzcwxbebalnhvps\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"dpjoofueam","domain":"ejwnealqvw.saleor.cloud"},{"saleor_api_url":"https://vxcdclzonj.saleor.cloud/graphql","token":"vxhxemgqtpvvmzgcrxxetsrecvmocg","jwks":"{\"keys\":
[{\"kty\": \"RSA\", \"key_ops\": [\"verify\"], \"n\": \"aofvflmughkjsiebpnlkakvjqywsbn\",
\"e\": \"AQAB\", \"use\": \"sig\", \"kid\": \"1\"}]}","saleor_app_id":"fxeyumrwrg","domain":"vxcdclzonj.saleor.cloud"}]}'
headers:
Connection:
- keep-alive
Content-Length:
- '779'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 17:07:43 GMT
allow:
- GET, POST, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 200
version: 1

View file

@ -1,44 +0,0 @@
interactions:
- request:
body: ''
headers:
accept:
- '*/*'
accept-encoding:
- gzip, deflate
authorization:
- Bearer access.token
connection:
- keep-alive
host:
- apl.example.com
user-agent:
- python-httpx/0.23.3
method: GET
uri: https://apl.example.com/api/v1/apl/aHR0cHM6Ly9ub24uZXhpc3RpbmcuZG9tYWlu
response:
content: '{"detail":"Not found."}'
headers:
Connection:
- keep-alive
Content-Length:
- '23'
Content-Type:
- application/json
Date:
- Sun, 19 Feb 2023 16:59:27 GMT
allow:
- GET, PUT, PATCH, DELETE, HEAD, OPTIONS
referrer-policy:
- same-origin
server:
- uvicorn
vary:
- Origin
x-content-type-options:
- nosniff
x-frame-options:
- DENY
http_version: HTTP/1.1
status_code: 404
version: 1

View file

@ -1,72 +0,0 @@
import random
import string
import pytest
from ..common import AplEntity
_SEED = 10
def _random_str(size: int, uppercase=False) -> str:
letters = string.ascii_uppercase if uppercase else string.ascii_lowercase
return "".join(random.choice(letters) for _ in range(size))
@pytest.fixture
def anyio_backend():
return "asyncio", {"use_uvloop": True}
@pytest.fixture(scope="module")
def vcr_config():
return {
"match_on": [
"method",
"scheme",
"host",
"port",
"path",
"query",
"headers",
"body",
]
}
jwks_template = """
{{
"keys": [
{{
"kty": "RSA",
"key_ops": ["verify"],
"n": "{key_n}",
"e": "AQAB",
"use": "sig",
"kid": "1"
}}
]
}}
"""
@pytest.fixture(scope="session")
def entities_factory():
def factory(size=10):
random.seed(_SEED)
return [
AplEntity(
saleor_api_url=f"https://{_random_str(10)}.saleor.cloud/graphql",
app_id=_random_str(10),
app_token=_random_str(30),
jwks=jwks_template.format(key_n=_random_str(30)),
)
for _ in range(size)
]
return factory
@pytest.fixture(scope="session")
def entity(entities_factory) -> AplEntity:
return entities_factory()[0]

View file

@ -1,134 +0,0 @@
from pathlib import Path
import pytest
from ..backends import FileAplBackend, MemAplBackend, RestAplBackend
from ..common import AplKeyError, NotConfiguredError
from ..wrapper import AplClient
pytestmark = pytest.mark.anyio
@pytest.fixture
def mem_apl_client():
client = AplClient()
client.setup("mem://")
return client
@pytest.fixture
def file_apl_client():
path_string = ".test_fileApl.json"
apl_file = Path(path_string)
client = AplClient()
client.setup(f"file://{path_string}")
yield client
if apl_file.exists():
apl_file.unlink()
@pytest.fixture
def rest_apl_client():
client = AplClient()
client.setup("https://access.token@apl.example.com")
return client
apl_clients = ["mem_apl_client", "file_apl_client", "rest_apl_client"]
def test_parse_settings_url_mem_backend():
backend = AplClient.parse_settings_url("mem://")
assert isinstance(backend, MemAplBackend)
@pytest.mark.parametrize(
"url,path",
[
("file://.fileApl.json", ".fileApl.json"),
("file://path/to/fileApl.json", "path/to/fileApl.json"),
],
)
def test_parse_settings_url_file_backend(url, path):
backend = AplClient.parse_settings_url(url)
assert isinstance(backend, FileAplBackend)
assert backend.path == Path(path)
@pytest.mark.parametrize(
"url,apl_url,token",
[
(
"https://86b0bb44ee7e488d9cc3949b78b0a3ac.6dnRhRlMhhtjHXwBh3f3lJkmz4opGX7EInHKvxlMyPq6T5Y7@apl.example.com",
"https://apl.example.com",
"86b0bb44ee7e488d9cc3949b78b0a3ac.6dnRhRlMhhtjHXwBh3f3lJkmz4opGX7EInHKvxlMyPq6T5Y7",
),
(
"http://access.token@localhost:8000",
"http://localhost:8000",
"access.token",
),
],
)
def test_parse_settings_url_rest_backend(url, apl_url, token):
backend = AplClient.parse_settings_url(url)
assert isinstance(backend, RestAplBackend)
assert backend.apl_url == apl_url
assert backend.token == token
def test_parse_settings_url_not_supported_backend():
with pytest.raises(NotImplementedError):
AplClient.parse_settings_url("new-schema://path")
async def test_apl_client_no_setup():
client = AplClient()
with pytest.raises(NotConfiguredError):
await client.get("shop.saleor.cloud")
@pytest.mark.vcr
@pytest.mark.parametrize("client_fixture", apl_clients)
async def test_apl_get(client_fixture, entity, request):
apl_client: AplClient = request.getfixturevalue(client_fixture)
await apl_client.set(entity.saleor_api_url, entity)
entity_from_client = await apl_client.get(entity.saleor_api_url)
assert entity_from_client == entity.copy()
@pytest.mark.vcr
@pytest.mark.parametrize("client_fixture", apl_clients)
async def test_apl_key_error(client_fixture, request):
apl_client: AplClient = request.getfixturevalue(client_fixture)
with pytest.raises(AplKeyError):
await apl_client.get("https://non.existing.domain")
@pytest.mark.vcr
@pytest.mark.parametrize("client_fixture", apl_clients)
async def test_apl_delete(client_fixture, entity, request):
apl_client: AplClient = request.getfixturevalue(client_fixture)
await apl_client.set(entity.saleor_api_url, entity)
await apl_client.delete(entity.saleor_api_url)
with pytest.raises(AplKeyError):
await apl_client.get(entity.saleor_api_url)
@pytest.mark.vcr
@pytest.mark.parametrize("client_fixture", apl_clients)
async def test_apl_delete_non_existing(client_fixture, request):
apl_client: AplClient = request.getfixturevalue(client_fixture)
with pytest.raises(AplKeyError):
await apl_client.delete("https://non.existing.domain")
@pytest.mark.vcr
@pytest.mark.parametrize("client_fixture", apl_clients)
async def test_apl_get_all(client_fixture, entities_factory, request):
apl_client: AplClient = request.getfixturevalue(client_fixture)
entities = entities_factory()
for entity in entities:
await apl_client.set(entity.saleor_api_url, entity)
apl_entities = [entity async for entity in apl_client.get_all(page_size=2)]
assert apl_entities == [(entity.saleor_api_url, entity) for entity in entities]

View file

@ -1,49 +0,0 @@
from pathlib import Path
from urllib.parse import urlparse
from .backends import AplBackend, FileAplBackend, MemAplBackend, RestAplBackend
from .common import AplEntity, NotConfiguredError
class AplClient:
def __init__(self):
self._backend: AplBackend | None = None
@staticmethod
def parse_settings_url(url: str) -> AplBackend:
parts = urlparse(url)
if parts.scheme == "file":
path = Path(url.removeprefix("file://"))
return FileAplBackend(path)
elif parts.scheme == "mem":
return MemAplBackend()
elif parts.scheme in ["http", "https"]:
token = parts.username or ""
port = "" if parts.port is None else f":{parts.port}"
apl_url = f"{parts.scheme}://{parts.hostname}{port}{parts.path}"
return RestAplBackend(apl_url=apl_url, token=token)
raise NotImplementedError()
def setup(self, url: str):
self._backend = self.parse_settings_url(url)
@property
def backend(self) -> AplBackend:
if self._backend is None:
raise NotConfiguredError(
"Run `apl_client.setup(...)` before using apl_client"
)
return self._backend
async def get(self, key: str) -> AplEntity:
return await self.backend.get(key)
async def set(self, key: str, value: AplEntity):
return await self.backend.set(key, value)
async def delete(self, key: str):
return await self.backend.delete(key)
async def get_all(self, page_size=100):
async for item in self.backend.get_all(page_size=page_size):
yield item

View file

@ -1,131 +0,0 @@
import logging
from fastapi import Depends, FastAPI, Request, status
from starlette.middleware.cors import CORSMiddleware
from .api import graphql_app
from .apl import AplEntity, apl_client
from .deps import (
ApiDependencies,
WebhookDependencies,
saleor_api_url_header,
verify_saleor_api_url,
)
from .integrations.datadog import DataDogClientError
from .logs import configure_logging
from .payload import OBSERVABILITY_EVENTS
from .saleor.client import GraphQLError, SaleorClient
from .saleor.common import InstallData, LazyUrl
from .settings import manifest, settings
from .utils import get_base_url
logger = logging.getLogger(__name__)
configure_logging(settings.debug)
apl_client.setup(settings.apl_url)
app = FastAPI(openapi_url="/api/openapi.json")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_headers=["*"],
allow_methods=["OPTIONS", "GET", "POST", "DELETE"],
)
@app.get("/manifest", status_code=status.HTTP_200_OK, tags=["installation-handshake"])
async def show_manifest(request: Request):
manifest_copy = manifest.copy()
for name, field in manifest_copy:
if isinstance(field, LazyUrl):
setattr(manifest_copy, name, field(request))
for extension in manifest_copy.extensions:
if isinstance(extension.url, LazyUrl):
extension.url = extension.url(request)
for webhook in manifest_copy.webhooks:
if isinstance(webhook.target_url, LazyUrl):
webhook.target_url = webhook.target_url(request)
return manifest_copy
@app.post(
"/install",
status_code=status.HTTP_200_OK,
tags=["installation-handshake"],
name="install",
)
async def install(
data: InstallData,
request: Request,
api_url=Depends(saleor_api_url_header),
_valid=Depends(verify_saleor_api_url),
):
client = SaleorClient(api_url, "test", data.auth_token)
try:
app_info = await client.app_info()
target_url = get_base_url(request).replace(path="webhooks")
await client.create_webhook(
target_url=str(target_url),
events=["OBSERVABILITY"],
name="OBSERVABILITY",
)
jwks = await client.get_jwks()
entity = AplEntity(
saleor_api_url=api_url,
app_id=app_info.id,
app_token=data.auth_token,
jwks=jwks,
)
await apl_client.set(api_url, entity)
except GraphQLError:
return {"error": {"message": "Wrong app token"}}
return {}
@app.post(
"/webhooks",
status_code=status.HTTP_200_OK,
tags=["webhooks"],
name="webhooks",
)
async def handle_observability_events(
payloads: list[OBSERVABILITY_EVENTS],
commons: WebhookDependencies = Depends(),
):
metadata = await commons.manager.get_metadata()
if not metadata.datadog or not metadata.datadog.active:
logger.warning(
"DataDog integration inactive or not configured. Dropping %s events",
len(payloads),
)
return
credentials = metadata.datadog.credentials
try:
logger.info("Sending %s events to DataDog", len(payloads))
await commons.datadog_client.send_logs(
commons.saleor_data.saleor_api_url, credentials, payloads
)
except DataDogClientError:
logger.warning("Sending logs to DataDog failed. Deactivating integration")
metadata.datadog.active = False
metadata.datadog.error = "Wrong credentials. Integration deactivated"
await commons.manager.save_private_metadata(metadata)
@app.get("/health", status_code=status.HTTP_200_OK, tags=["health"], name="health")
async def health():
return {"status": "ok"}
@app.get("/graphql")
async def graphiql(request: Request):
return await graphql_app.render_playground(request)
@app.post("/graphql")
async def graphql(request: Request, commons: ApiDependencies = Depends()):
request.state.api_context = commons
return await graphql_app.graphql_http_server(request)

View file

@ -1,143 +0,0 @@
import logging
from fastapi import Depends, Header, HTTPException, Request
from jwt.api_jwk import PyJWKSet
from .apl import AplEntity, AplError, AplKeyError, apl_client
from .integrations.datadog import DataDogApiClient, DataDogClient, TestDataDogClient
from .saleor import (
SALEOR_API_URL_HEADER,
SALEOR_SIGNATURE_HEADER,
SALEOR_TOKEN_HEADER,
SaleorToken,
)
from .saleor.crypto import decode_webhook_payload
from .saleor.manager import SaleorManager
from .schema import Metadata
from .settings import settings
logger = logging.getLogger(__name__)
async def saleor_api_url_header(
api_url: str | None = Header(None, alias=SALEOR_API_URL_HEADER)
) -> str:
if not api_url:
msg = f"Missing {SALEOR_API_URL_HEADER.upper()} header."
logger.warning(msg)
raise HTTPException(status_code=400, detail=msg)
return api_url
async def verify_saleor_api_url(api_url: str = Depends(saleor_api_url_header)):
# TODO implement
if api_url:
return True
raise HTTPException(
status_code=400,
detail=f"Provided url is invalid or environment {api_url} not allowed.",
)
async def saleor_token_header(
token: str | None = Header(None, alias=SALEOR_TOKEN_HEADER)
) -> str:
if not token:
msg = f"Missing {SALEOR_TOKEN_HEADER.upper()} header."
logger.warning(msg)
raise HTTPException(status_code=400, detail=msg)
return token
async def apl_entity(
api_url: str = Depends(saleor_api_url_header),
_verify=Depends(verify_saleor_api_url),
) -> AplEntity:
try:
return await apl_client.get(api_url)
except AplKeyError:
msg = f"App needs to be installed before use. First, install the app in Saleor at {api_url}."
logger.warning(msg)
raise HTTPException(status_code=400, detail=msg) # noqa: 904
except AplError as error:
logging.error("APL error %r", error)
raise HTTPException( # noqa: 904
status_code=500, detail="App error. Try again later."
)
async def saleor_token(
token_str: str = Depends(saleor_token_header),
saleor_data: AplEntity = Depends(apl_entity),
) -> SaleorToken:
token = SaleorToken(token_str, saleor_data.jwks)
# TODO handle jwks reload
return token
async def verify_is_stuff_user(token: SaleorToken = Depends(saleor_token)) -> bool:
if not token.is_staff_user():
HTTPException(
status_code=400, detail="Only staff user can perform this operation."
)
return True
async def get_saleor_signature(
saleor_signature: str | None = Header(None, alias=SALEOR_SIGNATURE_HEADER)
):
return saleor_signature
async def verify_webhook_signature(
request: Request,
jws: str = Depends(get_saleor_signature),
saleor_data: AplEntity = Depends(apl_entity),
):
saleor_jwks = PyJWKSet.from_dict(saleor_data.jwks)
return decode_webhook_payload(
jws=jws, jwks=saleor_jwks, webhook_payload=await request.body()
)
async def saleor_manager(saleor_data: AplEntity = Depends(apl_entity)):
return SaleorManager[Metadata](
saleor_data.saleor_api_url,
saleor_data.app_id,
saleor_data.app_token,
Metadata,
)
async def datadog_client() -> DataDogClient:
if settings.mock_datadog_client:
return TestDataDogClient()
return DataDogApiClient()
class ApiDependencies:
def __init__(
self,
token: SaleorToken = Depends(saleor_token),
saleor_data: AplEntity = Depends(apl_entity),
manager: SaleorManager[Metadata] = Depends(saleor_manager),
datadog_client: DataDogClient = Depends(datadog_client),
_verify_is_stuff_user=Depends(verify_is_stuff_user),
):
self.saleor_data = saleor_data
self.token = token
self.manager = manager
self.datadog_client = datadog_client
class WebhookDependencies:
def __init__(
self,
saleor_data: AplEntity = Depends(apl_entity),
manager: SaleorManager[Metadata] = Depends(saleor_manager),
datadog_client: DataDogClient = Depends(datadog_client),
_verify_webhook_signature=Depends(verify_webhook_signature),
):
self.saleor_data = saleor_data
self.manager = manager
self.datadog_client = datadog_client

View file

@ -1,145 +0,0 @@
import logging
from typing import Any, Awaitable, cast
from datadog_api_client.exceptions import ApiException, ForbiddenException
from datadog_api_client.v1.api.authentication_api import AuthenticationApi
from datadog_api_client.v2 import AsyncApiClient, Configuration
from datadog_api_client.v2.api.logs_api import LogsApi
from datadog_api_client.v2.model.content_encoding import ContentEncoding
from datadog_api_client.v2.model.http_log import HTTPLog
from pydantic import BaseModel
from ..payload import OBSERVABILITY_EVENTS
from .logs import generate_logs
LOGS_ENCODING = ContentEncoding("gzip")
logger = logging.getLogger(__name__)
LOGS_ERRORS = {
400: "Bad request (likely an issue in the payload formatting)",
401: "Unauthorized (likely a missing API Key)",
403: "Permission issue (likely using an invalid API Key)",
408: "Request Timeout, request should be retried after some time",
413: "Payload too large (batch is above 5MB uncompressed)",
429: "Too Many Requests, request should be retried after some time",
500: "Internal Server Error, the server encountered an unexpected condition that "
"prevented it from fulfilling the request, request should be retried after some time",
503: "Service Unavailable, the server is not ready to handle the request "
"probably because it is overloaded, request should be retried after some time",
}
class DataDogClientError(Exception):
pass
class DataDogCredsRejectedError(DataDogClientError):
"""DataDog credentials were rejected"""
class DatadogCredentials(BaseModel):
site: str
api_key: str
class DataDogClient:
async def validate_credentials(self, credentials: DatadogCredentials) -> bool:
raise NotImplementedError()
async def send_logs(
self,
saleor_api_url: str,
credentials: DatadogCredentials,
logs: list[OBSERVABILITY_EVENTS],
):
raise NotImplementedError()
class TestDataDogClient(DataDogClient):
_good_creds = DatadogCredentials(
site="US1", api_key="156e22d50c4e8b6816e1fd4794d3fd8c"
)
_failing_creds = DatadogCredentials(
site="EU1", api_key="156e22d50c4e8b6816e1fd4794d3fd8c"
)
async def validate_credentials(self, credentials: DatadogCredentials) -> bool:
return credentials == self._good_creds or credentials == self._failing_creds
async def send_logs(
self,
saleor_api_url: str,
credentials: DatadogCredentials,
logs: list[OBSERVABILITY_EVENTS],
):
if credentials == self._good_creds:
return
raise DataDogCredsRejectedError()
class DataDogApiClient(DataDogClient):
_site_map = {
"US1": "datadoghq.com",
"US3": "us3.datadoghq.com",
"US5": "us5.datadoghq.com",
"EU1": "datadoghq.eu",
"US1_FED": "ddog-gov.com",
}
@classmethod
def _get_config(cls, credentials: DatadogCredentials) -> Configuration:
configuration = Configuration()
configuration.api_key["apiKeyAuth"] = credentials.api_key
configuration.server_variables["site"] = cls._site_map[credentials.site]
return configuration
async def validate_credentials(self, credentials: DatadogCredentials) -> bool:
config = self._get_config(credentials)
async with AsyncApiClient(config) as api_client:
api_instance = AuthenticationApi(api_client)
try:
response = await cast(Awaitable[Any], api_instance.validate())
return response.valid
except ForbiddenException:
pass
except ApiException as exp:
logger.error(
"DataDog validate_credentials ApiException[%s]",
exp.status,
extra={
"status_code": exp.status,
"response_headers": dict(exp.headers) if exp.headers else None,
},
)
return False
async def send_logs(
self,
saleor_api_url: str,
credentials: DatadogCredentials,
logs: list[OBSERVABILITY_EVENTS],
):
config = self._get_config(credentials)
logs = generate_logs(logs, saleor_api_url)
async with AsyncApiClient(config) as api_client:
api_instance = LogsApi(api_client)
try:
await cast(
Awaitable[Any],
api_instance.submit_log(
body=HTTPLog(logs), content_encoding=LOGS_ENCODING
),
)
except ApiException as exp:
error_msg = LOGS_ERRORS.get(exp.status)
logger.error(
"DataDog send_logs ApiException[%s]",
error_msg,
extra={
"status_code": exp.status,
"response_headers": dict(exp.headers) if exp.headers else None,
},
)
except Exception:
logger.error("DataDog send_logs Unknown error")

View file

@ -1,291 +0,0 @@
from datetime import datetime
from enum import Enum
from datadog_api_client.v2.model.http_log_item import HTTPLogItem
from pydantic import BaseModel
from ua_parser import user_agent_parser # type: ignore
from ..payload import (
OBSERVABILITY_EVENTS,
ApiCallPayload,
EventDeliveryAttemptPayload,
JsonTruncText,
)
from ..utils import parse_headers
class EventDeliveryStatus(str, Enum):
pending = "pending"
success = "success"
failed = "failed"
class LogLevel(str, Enum):
info = "INFO"
warn = "WARN"
error = "ERROR"
log_level_map = {
EventDeliveryStatus.pending.value: LogLevel.warn,
EventDeliveryStatus.success.value: LogLevel.info,
EventDeliveryStatus.failed.value: LogLevel.error,
}
class DataDogLogUseragentDetails(BaseModel):
os: dict[str, str | None]
browser: dict[str, str | None]
device: dict[str, str | None]
class DataDogLogNetwork(BaseModel):
bytes_read: int | None
bytes_written: int | None
class DataDogLogEvent(BaseModel):
name: str
outcome: str | None
class DataDogLogHttp(BaseModel):
url: str
request_id: str | None
referer: str | None
status_code: str | None
method: str | None
useragent: str | None
useragent_details: DataDogLogUseragentDetails | None
class DataDogLog(BaseModel):
ddsource: str = "saleor"
service: str
timestamp: float
level: LogLevel = LogLevel.info
ddtags: str | None
http: DataDogLogHttp
network: DataDogLogNetwork
evt: DataDogLogEvent
duration: int | None
class GraphQLOperation(BaseModel):
name: str | None
operation_type: str
query: str | None
result: str | None
result_invalid: bool
truncated: bool
class EventRequest(BaseModel):
headers: dict[str, str] = {}
class EventResponse(BaseModel):
headers: dict[str, str] = {}
body: JsonTruncText | None
class App(BaseModel):
id: str
name: str
class ApiCall(BaseModel):
request_headers: dict[str, str] = {}
response_headers: dict[str, str] = {}
gql_operations: list[GraphQLOperation]
app: App | None
class ApiCallLog(DataDogLog):
saleor: ApiCall
class Webhook(BaseModel):
id: str
name: str
target_url: str
subscription_query: JsonTruncText | None
class EventDeliveryPayload(BaseModel):
content_length: int
body: JsonTruncText
class EventDelivery(BaseModel):
id: str
status: str
event_type: str
event_sync: bool
payload: EventDeliveryPayload
class EventDeliveryAttempt(BaseModel):
id: str
status: str
next_retry: datetime | None
request_headers: dict[str, str] = {}
response_headers: dict[str, str] = {}
response_body: JsonTruncText | None
class EventDeliveryAttemptData(BaseModel):
event_delivery_attempt: EventDeliveryAttempt
event_delivery: EventDelivery
webhook: Webhook
app: App
class EventDeliveryAttemptLog(DataDogLog):
saleor: EventDeliveryAttemptData
def http_status_ok(status_code: int | None):
if status_code is None or status_code >= 400:
return False
return True
def parse_useragent(useragent: str | None) -> DataDogLogUseragentDetails | None:
if not useragent:
return None
parsed = user_agent_parser.Parse(useragent)
return DataDogLogUseragentDetails(
os=parsed["os"],
browser=parsed["user_agent"],
device=parsed["device"],
)
def convert_duration(duration: float | None) -> float | None:
return duration * 1_000_000_000 if duration else None
def generate_api_call_log(payload: ApiCallPayload, saleor_domain: str) -> HTTPLogItem:
request_headers = parse_headers(payload.request.headers)
response_headers = parse_headers(payload.response.headers)
gql_operations: list[GraphQLOperation] = []
for op in payload.gql_operations:
gql_operations.append(
GraphQLOperation(
name=op.name.text if op.name else None,
operation_type=op.operation_type,
query=op.query.text if op.query else None,
result=op.result.text if op.result else None,
result_invalid=op.result_invalid,
truncated=any(
[
op.name.truncated if op.name else False,
op.query.truncated if op.query else False,
op.result.truncated if op.result else False,
]
),
)
)
level = (
LogLevel.info
if http_status_ok(payload.response.status_code)
else LogLevel.error
)
message = ApiCallLog(
service=saleor_domain,
timestamp=payload.request.time.timestamp() * 1000,
level=level,
http=DataDogLogHttp(
url=payload.request.url,
request_id=payload.request.id,
referer=request_headers.get("Referer"),
status_code=str(payload.response.status_code),
method=payload.request.method,
useragent=request_headers.get("User-Agent"),
useragent_details=parse_useragent(request_headers.get("User-Agent")),
),
network=DataDogLogNetwork(
bytes_read=payload.request.content_length,
bytes_written=payload.response.content_length,
),
evt=DataDogLogEvent(
name=payload.event_type,
outcome="success" if level == LogLevel.info else "failure",
),
saleor=ApiCall(
gql_operations=gql_operations,
app=App(id=payload.app.id, name=payload.app.name) if payload.app else None,
request_headers=dict(request_headers),
response_headers=dict(response_headers),
),
)
return HTTPLogItem(message=message.json())
def generate_event_delivery_attempt_log(
payload: EventDeliveryAttemptPayload, saleor_domain: str
) -> HTTPLogItem:
request_headers = parse_headers(payload.request.headers)
response_headers = parse_headers(payload.response.headers)
message = EventDeliveryAttemptLog(
service=saleor_domain,
timestamp=payload.time.timestamp() * 1000,
level=log_level_map.get(payload.event_delivery.status, LogLevel.error),
duration=convert_duration(payload.duration),
http=DataDogLogHttp(
url=payload.webhook.target_url,
request_id=payload.id,
status_code=str(payload.response.status_code),
),
network=DataDogLogNetwork(
bytes_read=payload.response.content_length,
bytes_written=payload.event_delivery.payload.content_length,
),
evt=DataDogLogEvent(
name=payload.event_type,
outcome=payload.status,
),
saleor=EventDeliveryAttemptData(
event_delivery_attempt=EventDeliveryAttempt(
id=payload.id,
status=payload.status,
next_retry=payload.next_retry,
request_headers=dict(request_headers),
response_headers=dict(response_headers),
response_body=payload.response.body,
),
app=App(id=payload.app.id, name=payload.app.name),
event_delivery=EventDelivery(
id=payload.event_delivery.id,
status=payload.event_delivery.status,
event_type=payload.event_delivery.event_type,
event_sync=payload.event_delivery.event_sync,
payload=EventDeliveryPayload(
body=payload.event_delivery.payload.body,
content_length=payload.event_delivery.payload.content_length,
),
),
webhook=Webhook(
id=payload.webhook.id,
name=payload.webhook.name,
target_url=payload.webhook.target_url,
subscription_query=payload.webhook.subscription_query,
),
),
)
return HTTPLogItem(message=message.json())
def generate_logs(
payloads: list[OBSERVABILITY_EVENTS], saleor_domain: str
) -> list[HTTPLogItem]:
log_items: list[HTTPLogItem] = []
for payload in payloads:
if isinstance(payload, ApiCallPayload):
log_items.append(generate_api_call_log(payload, saleor_domain))
elif isinstance(payload, EventDeliveryAttemptPayload):
log_items.append(
generate_event_delivery_attempt_log(payload, saleor_domain)
)
return log_items

View file

@ -1,62 +0,0 @@
import logging.config
def configure_logging(debug=False):
config = {
"version": 1,
"disable_existing_loggers": False,
"root": {
"level": "INFO",
"handlers": ["default"],
},
"formatters": {
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"datefmt": "%Y-%m-%dT%H:%M:%SZ",
"format": (
"%(asctime)s %(levelname)s %(lineno)s %(message)s %(name)s "
+ "%(pathname)s %(process)d %(threadName)s"
),
},
"verbose": {
"format": (
"%(levelname)s %(name)s %(message)s [PID:%(process)d:%(threadName)s]"
)
},
"uvicorn": {
"()": "uvicorn.logging.DefaultFormatter",
"fmt": "%(levelprefix)s %(message)s [PID:%(process)d:%(threadName)s]",
"use_colors": None,
},
},
"handlers": {
"default": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose" if debug else "json",
},
"uvicorn": {
"class": "logging.StreamHandler",
"formatter": "uvicorn" if debug else "json",
},
"null": {
"class": "logging.NullHandler",
},
},
"loggers": {
"uvicorn": {
"propagate": False,
"handlers": ["uvicorn"],
"level": "INFO",
},
"uvicorn.access": {
"propagate": False,
"handlers": ["null"],
},
"saleor_app_observability": {
"level": "DEBUG" if debug else "INFO",
"propagate": True,
},
},
}
logging.config.dictConfig(config)

View file

@ -1,93 +0,0 @@
from datetime import datetime
from typing import Any, Literal
from .utils import HttpHeaders, JsonBaseModel
class App(JsonBaseModel):
id: str
name: str
class ApiCallRequest(JsonBaseModel):
id: str
method: str
url: str
time: datetime
headers: HttpHeaders
content_length: int
class ApiCallResponse(JsonBaseModel):
headers: HttpHeaders
status_code: int | None
content_length: int
class JsonTruncText(JsonBaseModel):
text: str
truncated: bool
class GraphQLOperation(JsonBaseModel):
name: JsonTruncText | None
operation_type: str | None
query: JsonTruncText | None
result: JsonTruncText | None
result_invalid: bool
class ApiCallPayload(JsonBaseModel):
event_type: Literal["api_call"]
request: ApiCallRequest
response: ApiCallResponse
gql_operations: list[GraphQLOperation]
app: App | None
class EventDeliveryAttemptRequest(JsonBaseModel):
headers: HttpHeaders
class EventDeliveryAttemptResponse(JsonBaseModel):
headers: HttpHeaders
status_code: int | None
content_length: int
body: JsonTruncText
class EventDeliveryPayload(JsonBaseModel):
content_length: int
body: JsonTruncText
class EventDelivery(JsonBaseModel):
id: str
status: str
event_type: str
event_sync: bool
payload: EventDeliveryPayload
class Webhook(JsonBaseModel):
id: str
name: str
target_url: str
subscription_query: JsonTruncText | None
class EventDeliveryAttemptPayload(JsonBaseModel):
event_type: Literal["event_delivery_attempt"]
id: str
time: datetime
duration: float | None
status: str
next_retry: datetime | None
request: EventDeliveryAttemptRequest
response: EventDeliveryAttemptResponse
event_delivery: EventDelivery
webhook: Webhook
app: App
OBSERVABILITY_EVENTS = ApiCallPayload | EventDeliveryAttemptPayload | dict[str, Any]

View file

@ -1,13 +0,0 @@
from .common import (
SALEOR_API_URL_HEADER,
SALEOR_SIGNATURE_HEADER,
SALEOR_TOKEN_HEADER,
SaleorToken,
)
__all__ = [
"SALEOR_TOKEN_HEADER",
"SALEOR_API_URL_HEADER",
"SALEOR_SIGNATURE_HEADER",
"SaleorToken",
]

View file

@ -1,103 +0,0 @@
from typing import Any, Dict, Optional, Sequence
from urllib.parse import urlparse, urlunparse
import httpx
from .graphql import (
CREATE_WEBHOOK,
DELETE_PRIVATE_METADATA,
GET_APP_INFO,
UPDATE_PRIVATE_METADATA,
)
from .utils import JsonBaseModel
class WebhookInfo(JsonBaseModel):
id: str
target_url: str
is_active: bool
class AppInfo(JsonBaseModel):
id: str
webhooks: list[WebhookInfo]
private_metafields: dict[str, str]
class GraphQLError(Exception):
"""
Raised on Saleor GraphQL errors
"""
def __init__(
self,
errors: Sequence[Dict[str, Any]],
response_data: Optional[Dict[str, Any]] = None,
):
self.errors = errors
self.response_data = response_data
def __str__(self):
return (
f"GraphQLError: {', '.join([error['message'] for error in self.errors])}."
)
class SaleorClient:
def __init__(self, saleor_api_url: str, user_agent, auth_token=None):
self.saleor_api_url = saleor_api_url
self.headers = {"User-Agent": user_agent}
if auth_token:
self.headers["Authorization"] = f"Bearer {auth_token}"
async def get_jwks(self) -> dict[str, Any]:
parts = urlparse(self.saleor_api_url)
jwks_url = urlunparse(parts._replace(path="/.well-known/jwks.json"))
async with httpx.AsyncClient() as client:
res = await client.get(jwks_url)
return res.json()
async def execute(self, query, variables=None):
async with httpx.AsyncClient() as client:
data = {"query": query, "variables": variables}
res = await client.post(
self.saleor_api_url,
json=data,
headers=self.headers,
)
res_data = res.json()
if errors := res_data.get("errors"):
raise GraphQLError(errors=errors, response_data=res_data.get("data"))
return res_data["data"]
async def app_info(self) -> AppInfo:
result = await self.execute(GET_APP_INFO)
return AppInfo.parse_obj(result["app"])
async def update_private_metadata(self, app_id: str, metadata: dict[str, str]):
metadata_input = [{"key": key, "value": val} for key, val in metadata.items()]
await self.execute(
UPDATE_PRIVATE_METADATA,
variables={"appId": app_id, "metadata": metadata_input},
)
async def delete_private_metadata(self, app_id: str, keys: set[str]):
await self.execute(
DELETE_PRIVATE_METADATA, variables={"appId": app_id, "keys": list(keys)}
)
async def create_webhook(
self,
target_url: str,
events: list[str],
name: str,
):
webhook_input = {
"targetUrl": target_url,
"events": events,
"name": name,
}
await self.execute(
CREATE_WEBHOOK,
variables={"input": webhook_input},
)

View file

@ -1,151 +0,0 @@
from enum import Enum
from typing import Any
from fastapi import Request
from jwt.api_jwk import PyJWKSet
from pydantic import AnyHttpUrl, BaseModel, Field
from starlette.routing import NoMatchFound
from ..utils import get_base_url
from .crypto import decode_jwt
SALEOR_API_URL_HEADER = "saleor-api-url"
SALEOR_TOKEN_HEADER = "authorization-bearer"
SALEOR_SIGNATURE_HEADER = "saleor-signature"
class SaleorAppError(Exception):
"""Generic Saleor App Error, all framework errros inherit from this"""
class InstallAppError(SaleorAppError):
"""Install App error"""
class ConfigurationError(SaleorAppError):
"""App is misconfigured"""
class InstallData(BaseModel):
auth_token: str
class SaleorPermissions(str, Enum):
MANAGE_OBSERVABILITY = "MANAGE_OBSERVABILITY"
class LazyUrl(str):
"""
Used to declare a fully qualified url that is to be resolved when the
request is available.
"""
def __init__(self, name: str):
self.name = name
@classmethod
def __get_validators__(cls):
yield cls.validate
@classmethod
def validate(cls, v):
return v
def resolve(self):
return self.request.url_for(self.name)
def __call__(self, request: Request):
self.request = request
try:
return self.resolve()
except NoMatchFound:
raise ConfigurationError(
f"Failed to resolve a lazy url, check if an endpoint named '{self.name}' is defined."
) from None
def __hash__(self):
return hash(self.name)
def __eq__(self, other):
return self.name == other.name
def __ne__(self, other):
return not (self.name == other.name)
def __str__(self):
return f"LazyURL('{self.name}')"
def __repr__(self):
return str(self)
class LazyAbsoluteUrl(LazyUrl):
def resolve(self):
base_url = get_base_url(self.request)
return str(base_url.replace(path=self.name))
class LazyPath(LazyUrl):
"""
Much like LazyUrl but resolves only to the path part of an url.
The lazy aspect of this class is very redundant but is built like so to
maintain the same usage as the LazyUrl class.
"""
def resolve(self):
return self.request.app.url_path_for(self.name)
def __str__(self):
return f"LazyPath('{self.name}')"
class Webhook(BaseModel):
name: str
async_events: list[str] = Field(..., alias="asyncEvents")
query: str
target_url: AnyHttpUrl | LazyUrl = Field(..., alias="targetUrl")
is_active: bool = Field(..., alias="isActive")
class Config:
allow_population_by_field_name = True
class Manifest(BaseModel):
id: str
permissions: list[str]
name: str
author: str
version: str
about: str
extensions: list[Any] = []
webhooks: list[Webhook] = []
data_privacy: str = Field(..., alias="dataPrivacy")
data_privacy_url: AnyHttpUrl | LazyUrl = Field(..., alias="dataPrivacyUrl")
homepage_url: AnyHttpUrl | LazyUrl = Field(..., alias="homepageUrl")
support_url: AnyHttpUrl | LazyUrl = Field(..., alias="supportUrl")
configuration_url: AnyHttpUrl | LazyUrl | None = Field(
None, alias="configurationUrl"
)
app_url: AnyHttpUrl | LazyUrl = Field(..., alias="appUrl")
token_target_url: AnyHttpUrl | LazyUrl = Field(
LazyUrl("install"), alias="tokenTargetUrl"
)
class Config:
allow_population_by_field_name = True
class SaleorToken:
def __init__(self, token_str: str, jwks: dict[str, Any]):
self.token_str = token_str
self.jwks = PyJWKSet.from_dict(jwks)
self.jwt = decode_jwt(self.token_str, self.jwks)
def is_staff_user(self) -> bool:
return self.jwt["is_staff"]
def validate_permission(self, permissions: str | list[str]) -> bool:
return True
def validate_user_permission(self, permissions: str | list[str]) -> bool:
return True

View file

@ -1,66 +0,0 @@
from typing import Dict
from jwt.api_jwk import PyJWKSet
from jwt.api_jws import PyJWS
from jwt.api_jwt import PyJWT
class CryptoException(Exception):
"""
Base exception for the crypto module errors.
"""
class JWKSKeyMissing(CryptoException):
"""
Raised when a requested kid is missing from a keyset.
"""
class KeyIDMissing(CryptoException):
"""
Raised when a JWT without a 'kid' header is received.
"""
jwt_global_obj = PyJWT(options={"verify_signature": True})
jws_global_obj = PyJWS(options={"verify_signature": True})
get_unverified_header = jws_global_obj.get_unverified_header
def get_kid(sig_header: Dict[str, str]):
try:
return sig_header["kid"]
except KeyError as err:
raise KeyIDMissing() from err
def get_key_from_jwks(kid: str, jwks: PyJWKSet):
try:
jwks_key = jwks[kid]
except KeyError as err:
raise JWKSKeyMissing(f"The JWKS does not hold the key: {kid}") from err
return jwks_key.key
def decode_webhook_payload(jws: str, jwks: PyJWKSet, webhook_payload: bytes):
sig_header = get_unverified_header(jws)
key = get_key_from_jwks(kid=get_kid(sig_header), jwks=jwks)
return jws_global_obj.decode(
jws,
algorithms=[sig_header["alg"]],
key=key,
detached_payload=webhook_payload,
)
def decode_jwt(jwt: str, jwks: PyJWKSet):
sig_header = get_unverified_header(jwt)
key = get_key_from_jwks(kid=get_kid(sig_header), jwks=jwks)
return jwt_global_obj.decode(
jwt,
algorithms=[sig_header["alg"]],
key=key,
)

View file

@ -1,62 +0,0 @@
GET_APP_INFO = """
query GetAppInfo {
app {
id
webhooks {
id
targetUrl
isActive
}
privateMetafields
}
}
"""
UPDATE_PRIVATE_METADATA = """
mutation UpdatePrivateMetadata($appId: ID!, $metadata: [MetadataInput!]!) {
updatePrivateMetadata(id: $appId, input: $metadata) {
errors {
field
message
code
}
item {
privateMetafields
}
}
}
"""
DELETE_PRIVATE_METADATA = """
mutation DeletePrivateMetadata($appId: ID!, $keys: [String!]!) {
deletePrivateMetadata(id: $appId, keys: $keys) {
errors {
field
message
code
}
item {
privateMetafields
}
}
}
"""
CREATE_WEBHOOK = """
mutation WebhookCreate($input: WebhookCreateInput!) {
webhookCreate(input: $input) {
webhookErrors {
field
message
code
}
webhook {
id
}
}
}
"""

View file

@ -1,40 +0,0 @@
from typing import Generic, Type, TypeVar
from .client import AppInfo, SaleorClient
from .metadata import BaseMetadata
T = TypeVar("T", bound=BaseMetadata)
class SaleorManager(Generic[T]):
def __init__(
self,
saleor_api_url: str,
app_id: str,
auth_token: str,
metadata_cls: Type[T],
user_agent="test",
):
self.client = SaleorClient(saleor_api_url, user_agent, auth_token)
self.app_id = app_id
self.metadata_cls = metadata_cls
async def get_app_info(self) -> AppInfo:
return await self.client.app_info()
async def get_metadata(self) -> T:
app_info = await self.get_app_info()
return self.metadata_cls.parse_obj(app_info.private_metafields)
async def save_private_metadata(self, metadata: T, include: set[str] | None = None):
exported = metadata.export(include)
await self.client.update_private_metadata(self.app_id, exported)
async def delete_private_metadata(self, keys: str | set[str]):
await self.client.delete_private_metadata(self.app_id, {"datadog"})
flatten_keys: set[str] = set()
if isinstance(keys, str):
keys = {keys}
for key in keys:
flatten_keys.update(self.metadata_cls.field_flatten(key))
await self.client.delete_private_metadata(self.app_id, flatten_keys)

View file

@ -1,88 +0,0 @@
import json
from typing import Any
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel, root_validator, validator
from pydantic.fields import ModelField
class JsonFieldsModel(BaseModel):
@validator("*", pre=True)
def parse_field(cls, v):
if isinstance(v, str):
try:
return json.loads(v)
except json.JSONDecodeError:
pass
return v
class FlatMetadata(JsonFieldsModel):
pass
class BaseMetadata(JsonFieldsModel):
_flat_fields: dict[str, tuple[str, str]] = {}
@classmethod
def field_flatten(cls, field: str | ModelField) -> set[str]:
flatten_set: set[str] = set()
if isinstance(field, str):
field = cls.__fields__[field]
if issubclass(field.type_, FlatMetadata):
for sub_field in field.type_.__fields__.values():
flat_key = f"{field.name}_{sub_field.name}"
flatten_set.add(flat_key)
else:
flatten_set.add(field.name)
return flatten_set
@classmethod
def _get_flat_fields_map(cls) -> dict[str, tuple[str, str]]:
flat_fields: dict[str, tuple[str, str]] = {}
for field in cls.__fields__.values():
if issubclass(field.type_, FlatMetadata):
for sub_field in field.type_.__fields__.values():
flat_key = f"{field.name}_{sub_field.name}"
flat_fields[flat_key] = field.name, sub_field.name
return flat_fields
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._flat_fields = cls._get_flat_fields_map()
for field_name in cls.__fields__.keys():
if field_name in cls._flat_fields:
parent, subfield = cls._flat_fields[field_name]
raise ValueError(
f"{cls!r}: field {field_name} name conflict with flatten {parent}.{subfield}"
)
@staticmethod
def _encode_field(field, value):
if isinstance(value, FlatMetadata):
result = {}
for key, val in value.dict().items():
result[f"{field.name}_{key}"] = jsonable_encoder(val)
return result
return {field.name: jsonable_encoder(value)}
def export(self, include: set | None = None) -> dict[str, str]:
metadata: dict[str, Any] = {}
fields = set(self.__fields__.keys())
if include is not None:
fields = fields & include
for field_name in fields:
field, value = self.__fields__[field_name], getattr(self, field_name)
metadata.update(self._encode_field(field, value))
return {key: json.dumps(val) for key, val in metadata.items()}
@root_validator(pre=True)
def unflatten(cls, values):
unflatten_values: dict[str, Any] = {}
for key, val in values.items():
try:
parent_name, field_name = cls._flat_fields[key]
unflatten_values.setdefault(parent_name, {})[field_name] = val
except KeyError:
unflatten_values[key] = val
return unflatten_values

View file

@ -1,81 +0,0 @@
import pytest
from pydantic import BaseModel
from ..metadata import BaseMetadata, FlatMetadata
class Foo(BaseModel):
a: str
b: float | None = None
class Bar(BaseModel):
a: int
b: Foo | None = None
c: list[Foo] = []
class ExampleConfig(FlatMetadata):
login: str
password: str
more: Bar | None = None
class Metadata(BaseMetadata):
a: str | None = None
b: int | None = None
c: list[str] | None = None
d: str | None = None
e: Bar | None = None
f: ExampleConfig | None = None
g: list[Bar] = []
@pytest.fixture
def metadata():
return Metadata(
a="a",
b=3,
c=["a", "b", "c"],
e=Bar(a=0, b=Foo(a="a"), c=[Foo(a="a"), Foo(a="b")]),
f=ExampleConfig(
login="admin",
password="admin",
more=Bar(
a=1,
b=Foo(
a="a",
),
),
),
g=[Bar(a=0), Bar(a=1, b=Foo(a="a"))],
)
def test_to_metadata(metadata):
assert metadata.export() == {
"a": '"a"',
"b": "3",
"c": '["a", "b", "c"]',
"d": "null",
"e": '{"a": 0, "b": {"a": "a", "b": null}, "c": [{"a": "a", "b": null}, {"a": "b", "b": null}]}',
"f_login": '"admin"',
"f_password": '"admin"',
"f_more": '{"a": 1, "b": {"a": "a", "b": null}, "c": []}',
"g": '[{"a": 0, "b": null, "c": []}, {"a": 1, "b": {"a": "a", "b": null}, "c": []}]',
}
def test_metadata_export_reversible(metadata):
exported = metadata.export()
assert Metadata.parse_obj(exported) == metadata
def test_metadata_flatten_field(metadata):
assert metadata.field_flatten("a") == {"a"}
assert metadata.field_flatten("f") == {"f_login", "f_password", "f_more"}
assert metadata.field_flatten(metadata.__fields__["f"]) == {
"f_login",
"f_password",
"f_more",
}

View file

@ -1,12 +0,0 @@
from pydantic import BaseModel
def to_camel(snake_str: str) -> str:
components = snake_str.split("_")
return components[0] + "".join(x.capitalize() if x else "_" for x in components[1:])
class JsonBaseModel(BaseModel):
class Config:
alias_generator = to_camel
allow_population_by_field_name = True

View file

@ -1,62 +0,0 @@
interface Configuration {
active: Boolean!
error: String
}
type ConfigurationError {
field: String
message: String!
}
# Integration specific
# Datadog
enum DatadogSite{
US1
US3
US5
EU1
US1_FED
}
type DataDogCredentials {
site: DatadogSite!
apiKeyLast4: String!
}
type DatadogConfig implements Configuration {
active: Boolean!
error: String
credentials: DataDogCredentials!
}
type Integrations {
datadog: DatadogConfig
}
input DataDogCredentialsInput {
site: DatadogSite!
apiKey: String!
}
input DatadogConfigInput {
active: Boolean
credentials: DataDogCredentialsInput
}
type DataDogConfigMutationResult {
errors: [ConfigurationError!]!
datadog: DatadogConfig
}
# End integrations
type Query {
integrations: Integrations!
}
type Mutation {
updateDatadogConfig(input: DatadogConfigInput!): DataDogConfigMutationResult!
deleteDatadogConfig: DataDogConfigMutationResult!
}

View file

@ -1,12 +0,0 @@
from .integrations.datadog import DatadogCredentials
from .saleor.metadata import BaseMetadata, FlatMetadata
class DatadogConfig(FlatMetadata):
active: bool = False
error: str | None = None
credentials: DatadogCredentials
class Metadata(BaseMetadata):
datadog: DatadogConfig | None = None

View file

@ -1,43 +0,0 @@
from pathlib import Path
from pydantic import BaseSettings
from . import __version__ as app_version
from .saleor.common import LazyAbsoluteUrl, Manifest, SaleorPermissions
base_dir = Path(__file__).resolve().parent
class AppSettings(BaseSettings):
debug: bool = True
apl_url: str = f"file://{base_dir/'.fileApl.json'}"
mock_datadog_client = False
allowed_domains: set[str] = {"*"}
forbidden_domains: set[str] = set()
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = AppSettings()
manifest = Manifest(
id="saleor-app-monitoring",
name="Monitoring",
version=app_version,
about="Saleor Monitoring app",
data_privacy="",
app_url=LazyAbsoluteUrl("/"),
configuration_url=LazyAbsoluteUrl("/configuration"),
data_privacy_url="https://saleor.io/legal/privacy", # noqa
homepage_url="https://saleor.io/", # noqa
support_url="https://github.com/saleor", # noqa
token_target_url=LazyAbsoluteUrl("install"),
permissions=[SaleorPermissions.MANAGE_OBSERVABILITY],
extensions=[],
webhooks=[],
author="Saleor Commerce"
# TODO Add brand.logo.default
)

View file

@ -1,20 +0,0 @@
import pytest
from httpx import AsyncClient
from ..app import app
@pytest.fixture
def anyio_backend():
return "asyncio", {"use_uvloop": True}
@pytest.fixture
def observability_app():
return app
@pytest.fixture
async def client(observability_app):
async with AsyncClient(app=observability_app, base_url="http://test") as client:
yield client

View file

@ -1,8 +0,0 @@
import pytest
@pytest.mark.anyio
async def test_health(client):
response = await client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}

View file

@ -1,28 +0,0 @@
import pytest
from ..utils import domain_validation
@pytest.mark.parametrize(
"domain,allowed,forbidden,is_allowed",
[
("localhost:8000", {"*"}, None, True),
("wrong.com", {"example.com"}, None, False),
("sub.example.com", {"example.com"}, None, False),
("sub.example.com", {"*.example.com"}, None, True),
("sub.sub.example.com", {"*.example.com"}, None, True),
("example.com", {"*.example.com"}, None, False),
("sub.example.com", {"*.sub.example.com"}, None, False),
("sub.sub.example.com", {"*.sub.example.com"}, None, True),
("example.com", {"python.org", "example.com"}, None, True),
("any.com", {"python.org", "example.com", "*"}, None, True),
("example.com", {"*.example.com"}, None, False),
("sub.example.com", {"*"}, {"*.example.com"}, False),
("sub.example.com", {"*.example.com"}, {"*.banned.com"}, True),
("sub.example.com", {"*.example.com"}, {"x.example.com"}, True),
("non-eu-sub.saleor.cloud", {"*.saleor.cloud"}, {"*.eu.saleor.cloud"}, True),
("sub.eu.saleor.cloud", {"*.saleor.cloud"}, {"*.eu.saleor.cloud"}, False),
],
)
def test_domain_validation(domain, allowed, forbidden, is_allowed):
assert domain_validation(domain, allowed, forbidden) is is_allowed

View file

@ -1,48 +0,0 @@
import fnmatch
import urllib.parse
from fastapi import Request
from pydantic import BaseModel
from starlette.datastructures import URL
HttpHeaders = list[tuple[str, str]]
def domain_validation(
domain_name: str, allowed: set[str], forbidden: set[str] | None = None
):
forbidden = set() if forbidden is None else forbidden
for origin in allowed:
if fnmatch.fnmatchcase(domain_name, origin):
for disallowed in forbidden:
if fnmatch.fnmatchcase(domain_name, disallowed):
return False
return True
return False
def to_camel(snake_str: str) -> str:
components = snake_str.split("_")
return components[0] + "".join(x.capitalize() if x else "_" for x in components[1:])
class JsonBaseModel(BaseModel):
class Config:
alias_generator = to_camel
allow_population_by_field_name = True
def parse_headers(headers_list: HttpHeaders) -> dict[str, str]:
headers: dict[str, str] = {}
for key, val in headers_list:
headers[key] = val
return headers
def get_base_url(request: Request) -> URL:
base_url, headers = request.base_url, request.headers
scheme = headers.get("x-forwarded-proto", base_url.scheme).split(",")[0].strip()
if forwarded_host := headers.get("x-forwarded-host"):
parts = urllib.parse.urlsplit(f"//{forwarded_host}")
base_url = base_url.replace(hostname=parts.hostname, port=parts.port)
return base_url.replace(scheme=scheme)

File diff suppressed because it is too large Load diff

View file

@ -1,58 +0,0 @@
[tool.poetry]
name = "saleor-app-monitoring"
version = "0.1.0"
description = "Saleor Monitoring app"
authors = ["Przemysław Łada <przemyslaw.lada@saleor.io>"]
[tool.poetry.dependencies]
python = "^3.10"
fastapi = "^0.92.0"
uvicorn = {extras = ["standard"], version = "^0.20.0"}
python-dotenv = "^0.21.0"
python-json-logger = "^2.0.4"
datadog-api-client = {extras = ["async"], version = "^2.8.0"}
ua-parser = "^0.16.1"
ariadne = "^0.17.1"
httpx = "^0.23.3"
pyjwt = {extras = ["crypto"], version = "^2.6.0"}
[tool.poetry.group.dev.dependencies]
black = {extras = ["d"], version = "^22.12.0"}
pytest = "^7.2.0"
pre-commit = "^2.21.0"
ruff = "^0.0.215"
mypy = "^0.991"
anyio = "^3.6.2"
pytest-recording = "^0.12.2"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
target-version = "py310"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I001", # isort
"C", # flake8-comprehensions
"B", # flake8-bugbear
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"C901", # too complex
]
fix = true
fixable = ["I001"]
src = ["monitoring"]
[tool.ruff.isort]
known-first-party = ["monitoring"]
[tool.mypy]
allow_redefinition = true
show_error_codes = true
check_untyped_defs = true

View file

@ -1,18 +0,0 @@
version: "3.8"
services:
api:
build:
context: ./backend
dockerfile: Dockerfile
target: dev
stdin_open: true
tty: true
ports:
- "5001:80"
environment:
- DEBUG=True
# Uncomment to enable test credentials mode
# - MOCK_DATADOG_CLIENT=True
volumes:
- ./backend/monitoring/:/app/monitoring

View file

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -1,26 +0,0 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
transpilePackages: ["@saleor/apps-shared", "@saleor/apps-ui", "@saleor/react-hook-form-macaw"],
rewrites() {
/**
* For dev/preview Next.js can work as a proxy and redirect unknown paths to provided backend address
*
* In production, when env is not provided, frontend will call its relative path and reverse proxy will do the rest
*/
const backendPath = process.env.MONITORING_APP_API_URL;
if(!backendPath) {
throw new Error('Please set MONITORING_APP_API_URL variable')
}
return {
fallback: [
{
source: "/:path*",
destination: `${backendPath}/:path*`,
},
],
};
},
};

View file

@ -1,56 +0,0 @@
{
"name": "saleor-app-monitoring",
"version": "1.1.0",
"scripts": {
"build": "pnpm generate && next build",
"dev": "pnpm generate && NODE_OPTIONS='--inspect' next dev",
"generate": "graphql-codegen",
"lint": "next lint",
"lint:fix": "eslint --fix .",
"start": "next start",
"test": "vitest"
},
"dependencies": {
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.41.1",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@saleor/react-hook-form-macaw": "workspace:*",
"@urql/exchange-auth": "^2.1.4",
"@vitejs/plugin-react": "4.0.0",
"clsx": "^1.2.1",
"graphql": "16.6.0",
"graphql-tag": "^2.12.6",
"jsdom": "^20.0.3",
"next": "13.4.8",
"pino": "^8.14.1",
"pino-pretty": "^10.0.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"urql": "^4.0.4",
"vite": "4.3.9",
"vitest": "0.31.3"
},
"devDependencies": {
"@graphql-codegen/cli": "3.2.2",
"@graphql-codegen/introspection": "3.0.1",
"@graphql-codegen/schema-ast": "^3.0.1",
"@graphql-codegen/typed-document-node": "3.0.2",
"@graphql-codegen/typescript": "3.0.2",
"@graphql-codegen/typescript-operations": "3.0.2",
"@graphql-codegen/typescript-urql": "3.7.3",
"@graphql-typed-document-node/core": "3.2.0",
"@testing-library/react": "^13.4.0",
"@testing-library/react-hooks": "^8.0.1",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",
"eslint": "8.44.0",
"eslint-config-saleor": "workspace:*",
"typescript": "5.1.6"
},
"private": true
}

View file

@ -1,64 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.4, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 800.5 203.19" style="enable-background:new 0 0 800.5 203.19;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#632CA6;}
</style>
<g>
<g>
<path class="st0" d="M260.87,144.65h-37.4v-86.1h37.4c26.94,0,40.43,13.57,40.43,40.7C301.29,129.51,287.81,144.65,260.87,144.65z
M239.45,130.79h19c17.9,0,26.84-10.51,26.84-31.55c0-17.91-8.95-26.87-26.84-26.87h-19L239.45,130.79L239.45,130.79z"/>
<polygon class="st0" points="318.04,144.65 301.62,144.65 338.25,58.55 355.44,58.55 392.85,144.65 375.66,144.65 364.8,121.17
337.17,121.17 342.66,107.32 360.58,107.32 346.46,74.98 "/>
<polygon class="st0" points="383.82,58.55 449.28,58.55 449.28,72.39 424.55,72.39 424.55,144.65 408.57,144.65 408.57,72.39
383.82,72.39 "/>
<polygon class="st0" points="457.5,144.65 441.08,144.65 477.71,58.55 494.9,58.55 532.31,144.65 515.1,144.65 504.24,121.17
476.61,121.17 482.1,107.32 500.02,107.32 485.91,74.98 "/>
<path class="st0" d="M580.32,144.65h-37.4v-86.1h37.4c26.96,0,40.43,13.57,40.43,40.7C620.75,129.51,607.28,144.65,580.32,144.65z
M558.91,130.79h19c17.89,0,26.86-10.51,26.86-31.55c0-17.91-8.96-26.87-26.86-26.87h-19V130.79z"/>
<path class="st0" d="M631.58,101.72c0-29.2,14.45-43.79,43.33-43.79c28.44,0,42.64,14.59,42.64,43.79
c0,29.03-14.21,43.55-42.64,43.55C647.31,145.27,632.87,130.75,631.58,101.72z M674.91,131.39c17.36,0,26.05-10.01,26.05-30.05
c0-19.72-8.69-29.59-26.05-29.59c-17.82,0-26.73,9.87-26.73,29.59C648.18,121.38,657.09,131.39,674.91,131.39z"/>
<path class="st0" d="M784.26,109.81v20.16c-3.69,0.96-6.99,1.44-9.9,1.44c-19.55,0-29.31-10.34-29.31-31.01
c0-19.09,10.36-28.62,31.07-28.62c8.65,0,16.69,1.61,24.13,4.82V62.14c-7.44-2.8-15.89-4.21-25.34-4.21
c-30.97,0-46.46,14.15-46.46,42.47c0,29.9,15.22,44.87,45.67,44.87c10.47,0,19.17-1.52,26.13-4.58V95.64h-25.82l-5.4,14.16
L784.26,109.81L784.26,109.81z"/>
</g>
<g>
<g>
<path class="st0" d="M158.87,144.16L142,133.04l-14.07,23.5l-16.36-4.78l-14.41,21.99l0.74,6.92l78.33-14.43l-4.55-48.94
L158.87,144.16z M85.82,123.07l12.57-1.73c2.03,0.91,3.45,1.26,5.89,1.88c3.8,0.99,8.19,1.94,14.7-1.34
c1.51-0.75,4.67-3.64,5.94-5.28l51.49-9.34l5.25,63.57l-88.21,15.9L85.82,123.07z M181.46,100.16l-5.08,0.97L166.62,0.25
L0.25,19.54l20.5,166.33l19.47-2.83c-1.55-2.22-3.98-4.91-8.11-8.35c-5.74-4.76-3.71-12.86-0.32-17.97
c4.47-8.63,27.54-19.61,26.23-33.41c-0.47-5.02-1.27-11.55-5.93-16.03c-0.17,1.86,0.14,3.65,0.14,3.65s-1.91-2.44-2.87-5.77
c-0.95-1.28-1.69-1.68-2.7-3.39c-0.72,1.97-0.62,4.26-0.62,4.26s-1.56-3.7-1.82-6.82c-0.93,1.4-1.16,4.05-1.16,4.05
s-2.03-5.83-1.57-8.97c-0.93-2.73-3.68-8.15-2.9-20.47c5.08,3.56,16.26,2.71,20.61-3.71c1.45-2.13,2.44-7.93-0.72-19.36
c-2.03-7.33-7.05-18.25-9.01-22.4l-0.23,0.17c1.03,3.34,3.16,10.33,3.98,13.73c2.47,10.29,3.13,13.87,1.97,18.61
c-0.99,4.12-3.35,6.82-9.35,9.84c-6,3.03-13.96-4.34-14.47-4.74c-5.83-4.64-10.34-12.22-10.84-15.9
c-0.52-4.03,2.32-6.45,3.76-9.74c-2.05,0.59-4.34,1.63-4.34,1.63s2.73-2.83,6.1-5.27c1.4-0.92,2.21-1.51,3.68-2.73
c-2.13-0.03-3.86,0.02-3.86,0.02s3.55-1.92,7.23-3.31c-2.69-0.12-5.27-0.02-5.27-0.02S35.75,27.1,42,24.5
c4.3-1.76,8.5-1.24,10.86,2.17c3.1,4.47,6.35,6.9,13.25,8.41c4.24-1.88,5.52-2.84,10.84-4.29c4.68-5.15,8.36-5.82,8.36-5.82
s-1.82,1.67-2.31,4.3c2.66-2.09,5.57-3.84,5.57-3.84s-1.13,1.39-2.18,3.6l0.24,0.36c3.1-1.86,6.74-3.32,6.74-3.32
s-1.04,1.32-2.26,3.02c2.34-0.02,7.08,0.1,8.91,0.31c10.86,0.24,13.11-11.6,17.28-13.08c5.22-1.86,7.55-2.99,16.44,5.74
c7.63,7.5,13.59,20.91,10.63,23.92c-2.48,2.49-7.38-0.97-12.8-7.74c-2.87-3.58-5.03-7.81-6.05-13.19
c-0.86-4.54-4.19-7.17-4.19-7.17s1.93,4.31,1.93,8.11c0,2.08,0.26,9.84,3.59,14.19c-0.33,0.64-0.48,3.15-0.85,3.63
c-3.87-4.68-12.19-8.03-13.54-9.02c4.59,3.76,15.14,12.4,19.19,20.68c3.83,7.83,1.57,15.01,3.51,16.87
c0.55,0.53,8.24,10.11,9.72,14.93c2.58,8.39,0.15,17.21-3.22,22.68l-9.43,1.47c-1.38-0.38-2.31-0.58-3.55-1.29
c0.68-1.21,2.04-4.22,2.05-4.84l-0.53-0.93c-2.94,4.16-7.85,8.2-11.94,10.52c-5.35,3.03-11.51,2.56-15.52,1.32
c-11.39-3.51-22.16-11.21-24.75-13.23c0,0-0.08,1.61,0.41,1.98c2.87,3.24,9.45,9.1,15.81,13.18l-13.55,1.49l6.41,49.89
c-2.84,0.41-3.28,0.61-6.39,1.05c-2.74-9.68-7.98-16.01-13.71-19.69c-5.05-3.25-12.02-3.98-18.7-2.66l-0.43,0.5
c4.64-0.48,10.12,0.19,15.74,3.75c5.52,3.49,9.97,12.51,11.61,17.94c2.1,6.94,3.55,14.36-2.1,22.23
c-4.02,5.59-15.74,8.68-25.22,2c2.53,4.07,5.95,7.4,10.55,8.02c6.84,0.93,13.33-0.26,17.79-4.84c3.81-3.92,5.84-12.12,5.3-20.75
l6.03-0.87l2.18,15.49l99.88-12.03L181.46,100.16z M120.69,58.08c-0.28,0.64-0.72,1.05-0.06,3.12l0.04,0.12l0.1,0.27l0.27,0.62
c1.19,2.42,2.49,4.71,4.66,5.88c0.56-0.09,1.15-0.16,1.75-0.19c2.04-0.09,3.33,0.23,4.15,0.68c0.07-0.41,0.09-1,0.04-1.88
c-0.16-3.07,0.61-8.29-5.29-11.04c-2.23-1.03-5.35-0.72-6.39,0.58c0.19,0.02,0.36,0.06,0.49,0.11
C122.04,56.89,120.98,57.43,120.69,58.08 M137.23,86.73c-0.77-0.43-4.39-0.26-6.93,0.04c-4.84,0.57-10.07,2.25-11.22,3.14
c-2.08,1.61-1.14,4.42,0.4,5.57c4.32,3.22,8.1,5.39,12.09,4.86c2.45-0.32,4.61-4.2,6.14-7.73
C138.77,90.19,138.77,87.58,137.23,86.73 M94.36,61.88c1.37-1.3-6.8-3-13.14,1.32c-4.67,3.19-4.82,10.03-0.35,13.9
c0.45,0.38,0.82,0.66,1.16,0.88c1.31-0.62,2.8-1.24,4.51-1.79c2.9-0.94,5.3-1.43,7.28-1.68c0.95-1.06,2.05-2.92,1.77-6.29
C95.22,63.63,91.75,64.36,94.36,61.88"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 800.5 196.2" style="enable-background:new 0 0 800.5 196.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
</style>
<g>
<g>
<path class="st0" d="M167.2,98.4l-54.9,10c-1.4,1.8-4.7,4.8-6.3,5.6c-6.9,3.5-11.6,2.5-15.7,1.4c-2.6-0.7-4.1-1-6.3-2l-13.4,1.8
l8.1,67.9l94.1-17L167.2,98.4z M83.4,176.7l-0.8-7.4L98,145.9l17.4,5.1l15-25.1l18,11.9l13.7-28.7l4.9,52.2L83.4,176.7z M79.5,110
c-6.8-4.4-13.8-10.6-16.9-14.1c-0.5-0.4-0.4-2.1-0.4-2.1c2.8,2.2,14.3,10.4,26.4,14.1c4.3,1.3,10.9,1.8,16.6-1.4
c4.4-2.5,9.6-6.8,12.7-11.2l0.6,1c0,0.7-1.5,3.9-2.2,5.2c1.3,0.8,2.3,1,3.8,1.4l10.1-1.6c3.6-5.8,6.2-15.2,3.4-24.2
c-1.6-5.1-9.8-15.4-10.4-15.9c-2.1-2,0.3-9.6-3.7-18c-4.3-8.8-15.6-18-20.5-22.1c1.4,1,10.3,4.6,14.4,9.6c0.4-0.5,0.6-3.2,0.9-3.9
c-3.6-4.6-3.8-12.9-3.8-15.1c0-4.1-2.1-8.7-2.1-8.7s3.6,2.8,4.5,7.7c1.1,5.7,3.4,10.3,6.5,14.1c5.8,7.2,11,10.9,13.7,8.2
c3.2-3.2-3.2-17.5-11.3-25.5c-9.5-9.3-12-8.1-17.5-6.1c-4.4,1.5-6.8,14.2-18.4,13.9c-2-0.2-7-0.4-9.5-0.3c1.3-1.8,2.4-3.2,2.4-3.2
s-3.9,1.6-7.2,3.6l-0.3-0.4c1.1-2.4,2.3-3.8,2.3-3.8s-3.1,1.9-5.9,4.1c0.5-2.8,2.5-4.6,2.5-4.6s-3.9,0.7-8.9,6.2
c-5.7,1.5-7,2.6-11.6,4.6c-7.4-1.6-10.8-4.2-14.1-9c-2.5-3.6-7-4.2-11.6-2.3c-6.7,2.8-15.1,6.5-15.1,6.5s2.8-0.1,5.6,0
c-3.9,1.5-7.7,3.5-7.7,3.5s1.8-0.1,4.1,0c-1.6,1.3-2.4,1.9-3.9,2.9c-3.6,2.6-6.5,5.6-6.5,5.6s2.4-1.1,4.6-1.7
c-1.5,3.5-4.6,6.1-4,10.4c0.5,3.9,5.3,12,11.6,17c0.5,0.4,9,8.3,15.4,5.1c6.4-3.2,8.9-6.1,10-10.5c1.2-5.1,0.5-8.9-2.1-19.9
c-0.9-3.6-3.1-11.1-4.2-14.6l0.2-0.2c2.1,4.4,7.4,16.1,9.6,23.9c3.4,12.2,2.3,18.4,0.8,20.7c-4.7,6.8-16.6,7.8-22,4
c-0.8,13.1,2.1,18.9,3.1,21.8c-0.5,3.3,1.7,9.6,1.7,9.6s0.2-2.8,1.2-4.3c0.3,3.3,1.9,7.3,1.9,7.3s-0.1-2.4,0.7-4.5
c1.1,1.8,1.9,2.2,2.9,3.6c1,3.6,3,6.2,3,6.2s-0.3-1.9-0.2-3.9c5,4.8,5.8,11.8,6.3,17.1c1.4,14.7-23.2,26.4-28,35.6
c-3.6,5.4-5.8,14.1,0.3,19.2c14.8,12.3,9.1,15.7,16.5,21.1c10.2,7.4,22.9,4.1,27.2-1.9c6-8.4,4.5-16.3,2.2-23.7
c-1.8-5.8-6.5-15.4-12.4-19.1c-6-3.8-11.9-4.5-16.8-4l0.5-0.5c7.1-1.4,14.6-0.6,20,2.8c6.1,3.9,11.7,10.7,14.6,21
c3.3-0.5,3.8-0.7,6.8-1.1L65,111.6L79.5,110z M113.8,43.3c6.3,2.9,5.5,8.5,5.6,11.8c0.1,0.9,0,1.6-0.1,2c-0.9-0.5-2.2-0.8-4.4-0.7
c-0.6,0-1.3,0.1-1.9,0.2c-2.3-1.2-3.7-3.7-5-6.3c-0.1-0.2-0.2-0.5-0.3-0.7c0-0.1-0.1-0.2-0.1-0.3c0,0,0-0.1,0-0.1
c-0.7-2.2-0.2-2.7,0.1-3.3s1.4-1.3-0.2-1.8c-0.1,0-0.3-0.1-0.5-0.1C108.1,42.6,111.4,42.2,113.8,43.3z M106,79.9
c1.2-0.9,6.8-2.7,12-3.4c2.7-0.3,6.6-0.5,7.4,0c1.6,0.9,1.6,3.7,0.5,6.3c-1.6,3.8-3.9,7.9-6.6,8.2c-4.3,0.6-8.3-1.7-12.9-5.2
C104.8,84.6,103.8,81.6,106,79.9z M65.6,51.4c6.8-4.6,15.5-2.8,14-1.4c-2.8,2.7,0.9,1.9,1.3,6.8c0.3,3.6-0.9,5.6-1.9,6.7
c-2.1,0.3-4.7,0.8-7.8,1.8c-1.8,0.6-3.4,1.2-4.8,1.9c-0.4-0.2-0.8-0.5-1.2-0.9C60.5,62.1,60.7,54.8,65.6,51.4z"/>
</g>
<g>
<path class="st1" d="M249.2,141.8H211v-88h38.2c27.5,0,41.3,13.9,41.3,41.6C290.5,126.3,276.8,141.8,249.2,141.8z M227.4,127.6
h19.4c18.3,0,27.4-10.7,27.4-32.2c0-18.3-9.1-27.4-27.4-27.4h-19.4V127.6L227.4,127.6z"/>
<polygon class="st1" points="307.6,141.8 290.9,141.8 328.3,53.8 345.9,53.8 384.1,141.8 366.5,141.8 355.4,117.8 327.2,117.8
332.8,103.7 351.1,103.7 336.7,70.6 "/>
<polygon class="st1" points="374.8,53.8 441.7,53.8 441.7,68 416.5,68 416.5,141.8 400.1,141.8 400.1,68 374.8,68 "/>
<polygon class="st1" points="450.1,141.8 433.3,141.8 470.8,53.8 488.3,53.8 526.5,141.8 509,141.8 497.9,117.8 469.6,117.8
475.2,103.7 493.5,103.7 479.1,70.6 "/>
<path class="st1" d="M575.6,141.8h-38.2v-88h38.2c27.5,0,41.3,13.9,41.3,41.6C616.9,126.3,603.1,141.8,575.6,141.8z M553.7,127.6
h19.4c18.3,0,27.4-10.7,27.4-32.2c0-18.3-9.2-27.4-27.4-27.4h-19.4V127.6z"/>
<path class="st1" d="M628,97.9c0-29.8,14.8-44.7,44.3-44.7c29,0,43.6,14.9,43.6,44.7c0,29.7-14.5,44.5-43.6,44.5
C644,142.4,629.3,127.6,628,97.9z M672.2,128.2c17.7,0,26.6-10.2,26.6-30.7c0-20.2-8.9-30.2-26.6-30.2
c-18.2,0-27.3,10.1-27.3,30.2C644.9,118,654,128.2,672.2,128.2z"/>
<path class="st1" d="M783.9,106.2v20.6c-3.8,1-7.1,1.5-10.1,1.5c-20,0-30-10.6-30-31.7c0-19.5,10.6-29.2,31.7-29.2
c8.8,0,17,1.6,24.7,4.9V57.5c-7.6-2.9-16.2-4.3-25.9-4.3c-31.6,0-47.5,14.4-47.5,43.4c0,30.6,15.5,45.8,46.7,45.8
c10.7,0,19.6-1.6,26.7-4.7v-46h-26.4l-5.5,14.5H783.9z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -1,18 +0,0 @@
<svg width="409" height="137" viewBox="0 0 409 137" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M167.17 95.75C164.97 97.06 159.17 97.63 156.32 97.63C148.33 97.63 143.52 93.47 143.52 86.38V50.32L138.79 49.83V39.48H158.24V82.46C158.24 84.91 159.67 85.97 162.52 85.97H165.54L167.17 95.75Z" fill="#002E42"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M242.2 65.25H237.39C232.82 65.25 231.35 66.31 231.35 70.06C231.35 73.81 232.98 75.36 237.39 75.36C241.87 75.36 243.26 73.73 243.26 69.98C243.26 67.69 242.85 66.23 242.2 65.25ZM256.15 65.25C257.7 66.8 258.11 68.76 258.11 70.71C258.11 79.27 251.83 83.76 237.31 83.76C235.27 83.76 231.84 83.52 231.76 83.52C230.93 83.52 230.75 84 230.69 84.19C230.12 86.13 231.68 86.42 235.68 87.12L247.17 88.82C256.72 90.29 259.08 94.77 259.08 100.41C259.08 107.75 255.09 113.13 236.9 113.13C230.54 113.13 223.44 112.4 217.49 111.42V101.47C224.66 102.04 230.36 102.53 237.05 102.53C242.51 102.53 244.57 102.36 244.57 100.33C244.65 98.7 242.72 98.22 238.45 97.51L227.92 95.85C221.07 94.62 219.03 90.71 219.19 86.96C219.27 84.68 220.41 82.47 222.37 81.09C218.05 78.81 216.09 75.06 216.09 69.75C216.09 59.15 223.84 56.05 237.46 56.05H264.03V65.27H256.15V65.25Z" fill="#002E42"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M349.8 56.29H342.8H337.89V59.54V59.69V65.74V68.99H342.8V86.47V96.83H357.57V86.47V56.29H349.8Z" fill="#002E42"/>
<path d="M348.42 36.24C353.01 36.24 356.23 39.38 356.23 43.8C356.23 48.13 353 51.27 348.42 51.27C343.92 51.27 340.69 48.13 340.69 43.8C340.69 39.38 343.92 36.24 348.42 36.24Z" fill="#002E42"/>
<path d="M324.6 81.84C329.19 81.84 332.41 84.98 332.41 89.4C332.41 93.73 329.18 96.87 324.6 96.87C320.1 96.87 316.87 93.73 316.87 89.4C316.87 84.98 320.1 81.84 324.6 81.84Z" fill="#002E42"/>
<path d="M190.07 54.89C177.3 54.89 168.14 63.8 168.14 76.34C168.14 88.63 177.3 97.55 190.07 97.55C203.08 97.55 212.24 88.63 212.24 76.34C212.24 63.8 203.08 54.89 190.07 54.89ZM190.14 85.16C184.79 85.16 180.95 81.42 180.95 76.27C180.95 71.02 184.79 67.28 190.14 67.28C195.59 67.28 199.43 71.02 199.43 76.27C199.43 81.42 195.59 85.16 190.14 85.16Z" fill="#002E42"/>
<path d="M386 55.15C373.23 55.15 364.07 64.06 364.07 76.6C364.07 88.89 373.23 97.81 386 97.81C399.01 97.81 408.17 88.89 408.17 76.6C408.18 64.06 399.02 55.15 386 55.15ZM386.07 85.42C380.72 85.42 376.88 81.68 376.88 76.53C376.88 71.28 380.72 67.54 386.07 67.54C391.52 67.54 395.36 71.28 395.36 76.53C395.36 81.68 391.53 85.42 386.07 85.42Z" fill="#002E42"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M306.48 96.49H267.49V86.45L289.43 65.9H278.75H278.1H270.15V55.95H306.07V65.9L284.29 86.45L295.96 86.53V81.56H306.48V96.49Z" fill="#002E42"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 102.13L58.97 136.18L117.94 102.13V34.04L58.97 0L2.95 32.35C1.82 33 1.12 34.22 1.12 35.52C1.12 36.82 1.82 38.04 2.95 38.69L56.42 69.56V96.53L0 63.96V102.13ZM58.96 130.29L5.09 99.19V72.78L29.144 86.6631L29.14 86.67L58.77 104L89.86 86.06V50.17L58.78 32.23L30.3702 48.6309L7.64 35.51L58.97 5.88L112.83 36.99V99.19L58.96 130.29ZM35.4647 51.5717L58.97 65.14L82.2835 51.6749L58.78 38.11L35.4647 51.5717ZM84.77 56.1216L61.52 69.55V96.5348L84.77 83.12V56.1216Z" fill="#053447"/>
</g>
<defs>
<clipPath id="clip0">
<path d="M0 0H408.18V136.18H0V0Z" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 819 158.97"><defs><style>.cls-1{fill:#00ac69;}.cls-2{fill:#1ce783;}.cls-3{fill:#1d252c;}</style></defs><polygon class="cls-1" points="111.19 55.03 111.19 103.94 68.84 128.4 68.84 158.97 137.68 119.23 137.68 39.74 111.19 55.03"/><polygon class="cls-2" points="68.84 30.58 111.19 55.03 137.68 39.74 137.68 39.74 68.84 0 0 39.74 0 39.74 26.48 55.03 68.84 30.58"/><polygon class="cls-3" points="42.36 94.78 42.36 143.69 68.84 158.97 68.84 79.49 0 39.74 0 70.32 42.36 94.78"/><path class="cls-3" d="M242.17,50.14c-14.82,0-21.84,9.36-21.84,9.36h-.78L218,51.7H200.05v79.56h19.5v-46c0-10.14,7-17.16,17.16-17.16s17.16,7,17.16,17.16v46h19.5V83.68C273.37,63.4,260.11,50.14,242.17,50.14Z" transform="translate(-10 -10.51)"/><polygon class="cls-3" points="442.47 93.45 441.35 93.45 428.09 41.19 408.25 41.19 394.99 93.45 393.88 93.45 380.61 41.19 360.33 41.19 380.61 120.75 404.36 120.75 417.61 69.27 418.73 69.27 431.99 120.75 455.73 120.75 476.01 41.19 455.73 41.19 442.47 93.45"/><path class="cls-3" d="M545.72,58.72h-.78l-1.56-7H527v79.55h19.5v-46c0-10.14,4.68-14.82,14.82-14.82h10V51.71h-11.6A17.56,17.56,0,0,0,545.72,58.72Z" transform="translate(-10 -10.51)"/><path class="cls-3" d="M614.47,50.14c-23.39,0-40.55,17.16-40.55,41.34s16.19,41.34,40.55,41.34c19.73,0,31.61-11.61,36.56-20.15l-17.9-6.38c-1.77,3.24-8.91,9.47-18.66,9.47-11.37,0-19.49-7.12-21.05-18h59.27a35.38,35.38,0,0,0,.78-7.8C653.47,67.3,636.31,50.14,614.47,50.14ZM593.42,84.46c2.34-10.14,9.36-17.94,21.05-17.94,10.93,0,17.94,7.8,19.5,17.94Z" transform="translate(-10 -10.51)"/><path class="cls-3" d="M326.4,50.14c-23.4,0-40.56,17.16-40.56,41.34S302,132.82,326.4,132.82c19.73,0,31.6-11.61,36.55-20.15l-17.9-6.38c-1.77,3.24-8.9,9.47-18.65,9.47-11.37,0-19.5-7.12-21.06-18h59.28a35.38,35.38,0,0,0,.78-7.8C365.4,67.3,348.24,50.14,326.4,50.14ZM305.34,84.46c2.34-10.14,9.36-17.94,21.06-17.94,10.92,0,17.94,7.8,19.5,17.94Z" transform="translate(-10 -10.51)"/><rect class="cls-3" x="692.14" y="9.78" width="19.5" height="19.5"/><path class="cls-3" d="M775.45,114.88c-11.7,0-21.06-9.36-21.06-23.4s9.36-23.4,21.06-23.4,16.38,7.8,17.94,12.48l17.66-6.28c-4.28-11.11-14.78-24.14-35.6-24.14-23.4,0-40.56,17.16-40.56,41.34s17.16,41.34,40.56,41.34c21,0,31.5-13.24,35.7-24.88l-17.76-6.32C791.83,107.08,787.15,114.88,775.45,114.88Z" transform="translate(-10 -10.51)"/><polygon class="cls-3" points="645.63 27.11 656.7 27.11 656.7 120.75 676.2 120.75 676.2 9.78 645.63 9.78 645.63 27.11"/><rect class="cls-3" x="692.14" y="41.19" width="19.5" height="79.56"/><path class="cls-3" d="M821.59,116a7.52,7.52,0,1,0,7.41,7.52A7.28,7.28,0,0,0,821.59,116Zm0,13.89a6.37,6.37,0,1,1,6.26-6.37A6.12,6.12,0,0,1,821.59,129.85Z" transform="translate(-10 -10.51)"/><path class="cls-3" d="M824.82,122.13a2.64,2.64,0,0,0-2.82-2.62h-3.34v7.84h1.15v-2.72h1.05l2.71,2.72H825l-2.71-2.72A2.53,2.53,0,0,0,824.82,122.13Zm-5,1.35v-2.82H822a1.5,1.5,0,0,1,1.68,1.47c0,.83-.53,1.35-1.68,1.35Z" transform="translate(-10 -10.51)"/></svg>

Before

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1,17 +0,0 @@
import { DatadogSite } from "../generated/graphql";
export const API_KEYS_LINKS: { [key in DatadogSite]: string } = {
[DatadogSite.Us1]: "https://app.datadoghq.com/organization-settings/api-keys",
[DatadogSite.Us3]: "https://us3.datadoghq.com/organization-settings/api-keys",
[DatadogSite.Us5]: "https://us5.datadoghq.com/organization-settings/api-keys",
[DatadogSite.Eu1]: "https://app.datadoghq.eu/organization-settings/api-keys",
[DatadogSite.Us1Fed]: "https://app.ddog-gov.com/organization-settings/api-keys",
};
export const DATADOG_SITES_LINKS: { [key in DatadogSite]: string } = {
[DatadogSite.Us1]: "https://datadoghq.com",
[DatadogSite.Us3]: "https://us3.datadoghq.com",
[DatadogSite.Us5]: "https://us5.datadoghq.com",
[DatadogSite.Eu1]: "https://datadoghq.eu",
[DatadogSite.Us1Fed]: "https://ddog-gov.com",
};

View file

@ -1,60 +0,0 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { PropsWithChildren } from "react";
import { cacheExchange, createClient as urqlCreateClient, fetchExchange, Provider } from "urql";
import { authExchange } from "@urql/exchange-auth";
/**
* Local client creation. Contrary to other apps, Monitoring frontend doesnt contact Saleor directly,
* but calls Python-based service which also provides graphQL endpoint.
*
* App calls /graphql/ which is rewritten MONITORING_APP_API_URL. See next.config.js
*/
const createGraphQLClient = ({
graphql,
saleorApiUrl,
token,
}: {
graphql: string;
saleorApiUrl: string;
token: string;
}) => {
return urqlCreateClient({
url: graphql,
exchanges: [
cacheExchange,
authExchange(async (utils) => {
return {
addAuthToOperation(operation) {
const headers: Record<string, string> = token
? {
"Authorization-Bearer": token,
"Saleor-Api-Url": saleorApiUrl,
}
: {};
return utils.appendHeaders(operation, headers);
},
didAuthError(error) {
return error.graphQLErrors.some((e) => e.extensions?.code === "FORBIDDEN");
},
async refreshAuth() {},
};
}),
fetchExchange,
],
});
};
export function GraphQLProvider(props: PropsWithChildren<{}>) {
const { appBridgeState } = useAppBridge();
const saleorApiUrl = appBridgeState?.saleorApiUrl!;
const token = appBridgeState?.token!;
const client = createGraphQLClient({
saleorApiUrl,
token,
graphql: "/graphql/",
});
return <Provider value={client} {...props} />;
}

View file

@ -1,25 +0,0 @@
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useTheme } from "@saleor/macaw-ui/next";
import { useEffect } from "react";
// todo move to shared
export function ThemeSynchronizer() {
const { appBridgeState } = useAppBridge();
const { setTheme } = useTheme();
useEffect(() => {
if (!setTheme || !appBridgeState?.theme) {
return;
}
if (appBridgeState.theme === "light") {
setTheme("defaultLight");
}
if (appBridgeState.theme === "dark") {
setTheme("defaultDark");
}
}, [appBridgeState?.theme, setTheme]);
return null;
}

View file

@ -1,35 +0,0 @@
import "@saleor/macaw-ui/next/style";
import "../style.css";
import { AppBridge, AppBridgeProvider } from "@saleor/app-sdk/app-bridge";
import React from "react";
import { AppProps } from "next/app";
import { RoutePropagator } from "@saleor/app-sdk/app-bridge/next";
import { ThemeSynchronizer } from "../lib/theme-synchronizer";
import { Box, ThemeProvider } from "@saleor/macaw-ui/next";
import { NoSSRWrapper } from "@saleor/apps-shared";
import { GraphQLProvider } from "../graphql-provider";
/**
* Ensure instance is a singleton.
*/
export const appBridgeInstance = typeof window !== "undefined" ? new AppBridge() : undefined;
function NextApp({ Component, pageProps }: AppProps) {
return (
<NoSSRWrapper>
<AppBridgeProvider appBridgeInstance={appBridgeInstance}>
<GraphQLProvider>
<ThemeProvider>
<ThemeSynchronizer />
<RoutePropagator />
<Box padding={4}>
<Component {...pageProps} />
</Box>
</ThemeProvider>
</GraphQLProvider>
</AppBridgeProvider>
</NoSSRWrapper>
);
}
export default NextApp;

View file

@ -1,96 +0,0 @@
import { NextPage } from "next";
import React, { useEffect } from "react";
import { NoProvidersConfigured } from "../../ui/no-providers-configured";
import { useRouter } from "next/router";
import { DatadogConfig } from "../../ui/datadog/datadog-config";
import { DatadogSite, useConfigQuery } from "../../../generated/graphql";
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { DATADOG_SITES_LINKS } from "../../datadog-urls";
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import { Breadcrumbs, TextLink } from "@saleor/apps-ui";
const useActiveProvider = () => {
const router = useRouter();
const selectedProvider = router.query?.path && router.query.path[0];
return selectedProvider ?? null;
};
const ConfigurationPage = () => {
const [configuration, fetchConfiguration] = useConfigQuery();
const { appBridge } = useAppBridge();
const datadogCredentials = configuration.data?.integrations.datadog?.credentials;
const datadogError = configuration.data?.integrations.datadog?.error;
const { push } = useRouter();
useEffect(() => {
fetchConfiguration();
}, [fetchConfiguration]);
const selectedProvider = useActiveProvider();
if (configuration.fetching && !configuration.data) {
return <Text>Loading...</Text>;
}
if (selectedProvider === "datadog") {
return <DatadogConfig />;
}
if (!configuration.data?.integrations.datadog) {
return <NoProvidersConfigured />;
}
// when configured and everything is fine
if (datadogCredentials && !datadogError) {
const site = configuration.data?.integrations.datadog?.credentials.site ?? DatadogSite.Us1;
return (
<Box display={"flex"} gap={4} flexDirection={"column"}>
<Text as={"h1"} variant="heading">
App configured
</Text>
<Text as={"p"}>
Visit{" "}
<TextLink newTab href={DATADOG_SITES_LINKS[site] ?? "https://app.datadoghq.com/"}>
Datadog
</TextLink>{" "}
to access your logs
</Text>
<Button
onClick={() => {
push("/configuration/datadog");
}}
>
Edit configuration
</Button>
</Box>
);
}
if (datadogError) {
return (
<Box>
<Text variant="heading" as={"h1"}>
Configuration Error
</Text>
<Text color={"textCriticalDefault"}>{datadogError}</Text>
<Button
marginTop={8}
onClick={() => {
push("/configuration/datadog");
}}
>
Edit configuration
</Button>
</Box>
);
}
return null;
};
export default ConfigurationPage;

View file

@ -1,36 +0,0 @@
import { NextPage } from "next";
import { useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useEffect } from "react";
import { isInIframe } from "@saleor/apps-shared";
import { useRouter } from "next/router";
import { Box, Text } from "@saleor/macaw-ui/next";
/**
* This is page publicly accessible from your app.
* You should probably remove it.
*/
const IndexPage: NextPage = () => {
const { appBridgeState } = useAppBridge();
const { replace } = useRouter();
useEffect(() => {
if (appBridgeState?.ready) {
replace("/configuration");
}
}, [appBridgeState?.ready, replace]);
if (isInIframe()) {
return <Text>Loading...</Text>;
}
return (
<Box>
<Text variant="heading" as="h1">
Saleor Monitoring
</Text>
<Text>Install App in Saleor Dashboard to use it</Text>
</Box>
);
};
export default IndexPage;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -1,6 +0,0 @@
/**
* Add test setup logic here
*
* https://vitest.dev/config/#setupfiles
*/
export {};

View file

@ -1,3 +0,0 @@
a {
text-decoration: none;
}

View file

@ -1,251 +0,0 @@
import { useForm } from "react-hook-form";
import {
DataDogCredentialsInput,
DatadogSite,
useConfigQuery,
useDeleteDatadogCredentialsMutation,
useUpdateCredentialsMutation,
} from "../../../generated/graphql";
import { ArrowLeftIcon, Box, Button, Text } from "@saleor/macaw-ui/next";
import React, { useEffect } from "react";
import Image from "next/image";
import DatadogLogo from "../../assets/datadog/dd_logo_h_rgb.svg";
import { gql } from "urql";
import { actions, useAppBridge } from "@saleor/app-sdk/app-bridge";
import { useRouter } from "next/router";
import { API_KEYS_LINKS } from "../../datadog-urls";
import { useDashboardNotification } from "@saleor/apps-shared";
import { Input, Select, Toggle } from "@saleor/react-hook-form-macaw";
import { Breadcrumbs } from "@saleor/apps-ui";
gql`
query Config {
integrations {
datadog {
error
active
credentials {
apiKeyLast4
site
}
}
}
}
`;
gql`
mutation UpdateCredentials($input: DatadogConfigInput!) {
updateDatadogConfig(input: $input) {
datadog {
error
active
credentials {
apiKeyLast4
site
}
}
errors {
message
field
}
}
}
`;
gql`
mutation DeleteDatadogCredentials {
deleteDatadogConfig {
errors {
field
message
}
datadog {
credentials {
site
apiKeyLast4
}
}
}
}
`;
const buildMaskedKey = (keyLastChars: string) => `************${keyLastChars}`;
const ApiKeyHelperText = ({ site }: { site: DatadogSite }) => {
const url = API_KEYS_LINKS[site];
const { appBridge } = useAppBridge();
return (
<span>
Get one{" "}
<a
href={url}
onClick={(e) => {
e.preventDefault();
appBridge?.dispatch(
actions.Redirect({
to: url,
newContext: true,
})
);
}}
>
here
</a>
</span>
);
};
export const DatadogConfig = () => {
const [queryData, fetchConfig] = useConfigQuery();
const [, mutateCredentials] = useUpdateCredentialsMutation();
const [, deleteCredentials] = useDeleteDatadogCredentialsMutation();
const router = useRouter();
const { notifyError, notifySuccess } = useDashboardNotification();
const { register, handleSubmit, setValue, control, reset, watch } = useForm<
DataDogCredentialsInput & {
active: boolean;
}
>({
defaultValues: {
site: DatadogSite.Us1,
apiKey: "",
active: true,
},
});
const activeSite = watch("site");
useEffect(() => {
fetchConfig();
}, [fetchConfig]);
const updateValuesToCurrentConfig = () => {
const datadogConfig = queryData.data?.integrations.datadog;
if (!datadogConfig) {
return;
}
setValue("active", datadogConfig.active);
setValue("apiKey", buildMaskedKey(datadogConfig.credentials.apiKeyLast4));
setValue("site", datadogConfig.credentials.site);
};
useEffect(() => {
const datadogConfig = queryData.data?.integrations.datadog;
if (datadogConfig) {
updateValuesToCurrentConfig();
}
}, [queryData.data, setValue]);
if (queryData.fetching && !queryData.data) {
return <Text>Loading</Text>;
}
return (
<Box>
<Breadcrumbs>
<Breadcrumbs.Item href={"/configuration"}>Configuration</Breadcrumbs.Item>
<Breadcrumbs.Item>DataDog</Breadcrumbs.Item>
</Breadcrumbs>
<Box marginTop={8} display={"grid"} __gridTemplateColumns={"400px auto"} gap={8}>
<Box display={"flex"} gap={4} flexDirection={"column"}>
<Text variant={"heading"} as={"h1"}>
Configuration
</Text>
<Image width={100} src={DatadogLogo} alt="DataDog" />
<Text as={"p"}>
Configure your Datadog integration to send your Saleor metrics to Datadog.
</Text>
</Box>
<Box
display={"flex"}
gap={4}
flexDirection={"column"}
as={"form"}
borderColor={"neutralHighlight"}
borderWidth={1}
borderStyle={"solid"}
borderRadius={4}
padding={8}
onSubmit={handleSubmit((values) => {
return mutateCredentials({
input: {
active: values.active,
credentials: {
apiKey: values.apiKey,
site: values.site,
},
},
}).then((res) => {
const updatedConfig = res.data?.updateDatadogConfig.datadog;
const errors = res.data?.updateDatadogConfig.errors;
if (updatedConfig) {
setValue("active", updatedConfig.active);
setValue("apiKey", buildMaskedKey(updatedConfig.credentials.apiKeyLast4));
setValue("site", updatedConfig.credentials.site);
notifySuccess("Configuration updated", "Successfully updated Datadog settings");
}
if (errors?.length) {
notifyError("Error configuring Datadog", errors[0].message);
}
});
})}
>
<Box as={"label"} display={"flex"} gap={2}>
<Toggle control={control} name={"active"} />
<Text variant={"bodyEmp"}>Active</Text>
</Box>
<Select
label={"Datadog Site"}
options={Object.values(DatadogSite).map((v) => ({
label: v,
value: v,
}))}
control={control}
name={"site"}
/>
<Input
label="API Key"
defaultValue=""
helperText={<ApiKeyHelperText site={activeSite} />}
control={control}
name={"apiKey"}
/>
{queryData.data?.integrations.datadog?.error && (
<Text color={"textCriticalDefault"}>{queryData.data?.integrations.datadog?.error}</Text>
)}
<Box display={"flex"} gap={2} marginTop={8} justifyContent={"flex-end"}>
<Button
variant={"tertiary"}
type="reset"
onClick={(e) => {
e.preventDefault();
deleteCredentials({}).then(() => {
fetchConfig();
reset();
notifySuccess("Configuration updated", "Successfully deleted Datadog settings");
router.push("/configuration");
});
}}
>
<Text color={"textCriticalDefault"}>Delete configuration</Text>
</Button>
<Button type="submit">Save configuration</Button>
</Box>
</Box>
</Box>
</Box>
);
};

View file

@ -1,14 +0,0 @@
import { Box, Button, Text } from "@saleor/macaw-ui/next";
import Link from "next/link";
export const NoProvidersConfigured = () => (
<Box display={"flex"} gap={4} flexDirection={"column"}>
<Text as={"h1"} variant="heading">
No providers configured
</Text>
<Text as={"p"}>You need to configure Datadog to enable the app</Text>
<Link href={"/configuration/datadog"}>
<Button>Configure Datadog</Button>
</Link>
</Box>
);

View file

@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View file

@ -1,27 +0,0 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"pipeline": {
"build": {
"env": [
"APL",
"APP_DEBUG",
"NODE_ENV",
"SECRET_KEY",
"ALLOWED_DOMAIN_PATTERN",
"REST_APL_ENDPOINT",
"REST_APL_TOKEN",
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_DSN",
"NEXT_PUBLIC_SENTRY_DSN",
"SENTRY_ORG",
"SENTRY_PROJECT",
"SENTRY_AUTH_TOKEN",
"NEXT_PUBLIC_VERCEL_ENV",
"MONITORING_APP_API_URL",
"APP_IFRAME_BASE_URL",
"APP_API_BASE_URL"
]
}
}
}

View file

@ -1,13 +0,0 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
passWithNoTests: true,
environment: "jsdom",
setupFiles: "./src/setup-tests.ts",
css: false,
},
});

View file

@ -1017,124 +1017,6 @@ importers:
specifier: 5.1.6 specifier: 5.1.6
version: 5.1.6 version: 5.1.6
apps/monitoring:
dependencies:
'@material-ui/core':
specifier: ^4.12.4
version: 4.12.4(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@material-ui/icons':
specifier: ^4.11.3
version: 4.11.3(@material-ui/core@4.12.4)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@material-ui/lab':
specifier: 4.0.0-alpha.61
version: 4.0.0-alpha.61(@material-ui/core@4.12.4)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@saleor/app-sdk':
specifier: 0.41.1
version: 0.41.1(next@13.4.8)(react-dom@18.2.0)(react@18.2.0)
'@saleor/apps-shared':
specifier: workspace:*
version: link:../../packages/shared
'@saleor/apps-ui':
specifier: workspace:*
version: link:../../packages/ui
'@saleor/macaw-ui':
specifier: 0.8.0-pre.95
version: 0.8.0-pre.95(@types/react-dom@18.2.5)(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@saleor/react-hook-form-macaw':
specifier: workspace:*
version: link:../../packages/react-hook-form-macaw
'@urql/exchange-auth':
specifier: ^2.1.4
version: 2.1.4(graphql@16.6.0)
'@vitejs/plugin-react':
specifier: 4.0.0
version: 4.0.0(vite@4.3.9)
clsx:
specifier: ^1.2.1
version: 1.2.1
graphql:
specifier: 16.6.0
version: 16.6.0
graphql-tag:
specifier: ^2.12.6
version: 2.12.6(graphql@16.6.0)
jsdom:
specifier: ^20.0.3
version: 20.0.3
next:
specifier: 13.4.8
version: 13.4.8(@babel/core@7.22.8)(react-dom@18.2.0)(react@18.2.0)
pino:
specifier: ^8.14.1
version: 8.14.1
pino-pretty:
specifier: ^10.0.0
version: 10.0.0
react:
specifier: 18.2.0
version: 18.2.0
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-hook-form:
specifier: ^7.43.9
version: 7.44.3(react@18.2.0)
urql:
specifier: ^4.0.4
version: 4.0.4(graphql@16.6.0)(react@18.2.0)
vite:
specifier: 4.3.9
version: 4.3.9(@types/node@18.15.3)
vitest:
specifier: 0.31.3
version: 0.31.3(jsdom@20.0.3)
devDependencies:
'@graphql-codegen/cli':
specifier: 3.2.2
version: 3.2.2(@babel/core@7.22.8)(@types/node@18.15.3)(graphql@16.6.0)
'@graphql-codegen/introspection':
specifier: 3.0.1
version: 3.0.1(graphql@16.6.0)
'@graphql-codegen/schema-ast':
specifier: ^3.0.1
version: 3.0.1(graphql@16.6.0)
'@graphql-codegen/typed-document-node':
specifier: 3.0.2
version: 3.0.2(graphql@16.6.0)
'@graphql-codegen/typescript':
specifier: 3.0.2
version: 3.0.2(graphql@16.6.0)
'@graphql-codegen/typescript-operations':
specifier: 3.0.2
version: 3.0.2(graphql@16.6.0)
'@graphql-codegen/typescript-urql':
specifier: 3.7.3
version: 3.7.3(graphql-tag@2.12.6)(graphql@16.6.0)
'@graphql-typed-document-node/core':
specifier: 3.2.0
version: 3.2.0(graphql@16.6.0)
'@testing-library/react':
specifier: ^13.4.0
version: 13.4.0(react-dom@18.2.0)(react@18.2.0)
'@testing-library/react-hooks':
specifier: ^8.0.1
version: 8.0.1(@types/react@18.2.5)(react-dom@18.2.0)(react@18.2.0)
'@types/react':
specifier: 18.2.5
version: 18.2.5
'@types/react-dom':
specifier: 18.2.5
version: 18.2.5
eslint:
specifier: 8.44.0
version: 8.44.0
eslint-config-saleor:
specifier: workspace:*
version: link:../../packages/eslint-config-saleor
typescript:
specifier: 5.1.6
version: 5.1.6
apps/products-feed: apps/products-feed:
dependencies: dependencies:
'@aws-sdk/client-s3': '@aws-sdk/client-s3':