diff --git a/.github/workflows/dev-deploy.yml b/.github/workflows/dev-deploy.yml index f395e7884..7e168b4f7 100644 --- a/.github/workflows/dev-deploy.yml +++ b/.github/workflows/dev-deploy.yml @@ -8,7 +8,7 @@ on: required: false type: string push: - branches: ["main"] + branches: ["main", "infra/*"] permissions: contents: read diff --git a/apps/app/.env.dev3 b/apps/app/.env.dev3 index b250921df..d9cb4464e 100644 --- a/apps/app/.env.dev3 +++ b/apps/app/.env.dev3 @@ -1,3 +1,5 @@ # Development environment variables for the Doenet app. # This file is intentionally committed — all VITE_ vars are public. VITE_DISCOURSE_URL=https://dev3-community.doenet.org/ +VITE_UMAMI_SCRIPT_URL=https://umami.dev3.doenet.org/script.js +VITE_UMAMI_WEBSITE_ID=9c76c8c9-9a83-47f1-80ce-85865a0e5c0d diff --git a/apps/app/index.html b/apps/app/index.html index a49cd2b9f..9e6366340 100644 --- a/apps/app/index.html +++ b/apps/app/index.html @@ -1,20 +1,6 @@ - - - Doenet diff --git a/apps/app/src/index.tsx b/apps/app/src/index.tsx index 1b3f1e097..f8b5b4185 100644 --- a/apps/app/src/index.tsx +++ b/apps/app/src/index.tsx @@ -128,6 +128,7 @@ import { RawViewer, loader as rawViewerLoader } from "./paths/RawViewer"; import { GetInvolved } from "./paths/GetInvolved"; import { Events } from "./paths/Events"; import { QuickLinks } from "./paths/QuickLinks"; +import { initializeAnalytics } from "./utils/analytics"; const router = createBrowserRouter([ { @@ -436,6 +437,8 @@ const router = createBrowserRouter([ }, ]); +initializeAnalytics(); + const root = createRoot(document.getElementById("root")!); root.render(); diff --git a/apps/app/src/utils/analytics.ts b/apps/app/src/utils/analytics.ts new file mode 100644 index 000000000..c7277fae1 --- /dev/null +++ b/apps/app/src/utils/analytics.ts @@ -0,0 +1,20 @@ +const umamiScriptUrl = import.meta.env.VITE_UMAMI_SCRIPT_URL?.trim(); +const umamiWebsiteId = import.meta.env.VITE_UMAMI_WEBSITE_ID?.trim(); + +export function initializeAnalytics() { + if (!umamiScriptUrl || !umamiWebsiteId) { + return; + } + + if (document.querySelector('script[data-doenet-analytics="umami"]')) { + return; + } + + const script = document.createElement("script"); + script.defer = true; + script.src = umamiScriptUrl; + script.dataset.doenetAnalytics = "umami"; + script.setAttribute("data-website-id", umamiWebsiteId); + + document.head.append(script); +} diff --git a/apps/app/src/vite-env.d.ts b/apps/app/src/vite-env.d.ts new file mode 100644 index 000000000..948f9df6b --- /dev/null +++ b/apps/app/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_UMAMI_SCRIPT_URL?: string; + readonly VITE_UMAMI_WEBSITE_ID?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/infra/README.md b/infra/README.md index 6e7f209ef..3774df3d8 100644 --- a/infra/README.md +++ b/infra/README.md @@ -4,3 +4,69 @@ To deploy changes to AWS, run the `aws-deploy` script. To lint CloudFormation templates without deploying them, use `cfn-lint`. https://github.com/aws-cloudformation/cfn-lint + +## dev3 Umami services + +The `dev3` environment now includes two additional ECS service stacks: + +- `dev3-doenet-umami-db` - internal Postgres for Umami +- `dev3-doenet-umami` - public Umami app at `umami.dev3.doenet.org` + +These stacks are deployed through the existing `infra\dev3.aws` stack order. + +The Umami listener rule must have a **higher precedence** than the generic Doenet `/api` listener rule. In the current `dev3` params, that means Umami uses priority `1` and the generic Doenet API service uses priority `10`. + +### Required SSM parameters before deploying the services + +Create these Parameter Store entries in `us-east-2`: + +- `/${EnvironmentName}/umami/DBPassword` - SecureString used by the Postgres container +- `/${EnvironmentName}/umami/DatabaseUrl` - SecureString used by the Umami app container +- `/${EnvironmentName}/umami/AppSecret` - SecureString used by Umami + +For `dev3`, that means: + +- `/dev3/umami/DBPassword` +- `/dev3/umami/DatabaseUrl` +- `/dev3/umami/AppSecret` + +`DatabaseUrl` should point at the internal Postgres service. With the current `dev3` Cloud Map namespace, use: + +`postgresql://umami:@umami-db.dev3.doenet.internal:5432/umami` + +Do not use the short host `umami-db` here; the Umami task needs the full Cloud Map hostname. + +### Frontend analytics config after Umami bootstrap + +Frontend Umami settings are committed in `apps/app/.env.dev3`, not stored in SSM. + +The committed defaults should look like: + +- `VITE_UMAMI_SCRIPT_URL=https://umami.dev3.doenet.org/script.js` +- `VITE_UMAMI_WEBSITE_ID=` + +If `VITE_UMAMI_WEBSITE_ID` is blank, the `dev3` app builds without loading Umami. + +### First Umami login and website setup + +On a fresh Umami database, the default login is: + +- username: `admin` +- password: `umami` + +After the first login: + +1. Change the default admin password immediately. +2. Create a new website entry for the React app. +3. Set the website domain to the app hostname you want to track in `dev3`. +4. Copy the generated website ID from Umami. +5. Update `apps/app/.env.dev3`: + - `VITE_UMAMI_SCRIPT_URL=https://umami.dev3.doenet.org/script.js` + - `VITE_UMAMI_WEBSITE_ID=` +6. Commit that change and redeploy the frontend so the `dev3` app build picks up the website ID. + +If the default admin account is not present, Umami may not have completed its first-run database initialization. Check the Umami container logs and the database connectivity/config first. + +For additional Umami setup details, see the upstream docs: + +- https://umami.is/docs diff --git a/infra/cloudformation/dev3-doenet-umami-db.params b/infra/cloudformation/dev3-doenet-umami-db.params new file mode 100644 index 000000000..38538267e --- /dev/null +++ b/infra/cloudformation/dev3-doenet-umami-db.params @@ -0,0 +1,138 @@ +[ + { + "ParameterKey": "RedshiftSecretArn", + "ParameterValue": "notapplicable" + }, + { + "ParameterKey": "DomainNames", + "ParameterValue": "notapplicable" + }, + { + "ParameterKey": "EnvironmentName", + "ParameterValue": "dev3" + }, + { + "ParameterKey": "ServiceName", + "ParameterValue": "umami-db" + }, + { + "ParameterKey": "S3AppBucket", + "ParameterValue": "/dev3/S3Bucket" + }, + { + "ParameterKey": "CloudMapPrivateNamespace", + "ParameterValue": "/dev3/CloudMapPrivateNamespace" + }, + { + "ParameterKey": "Priority", + "ParameterValue": "2" + }, + { + "ParameterKey": "EcsLaunchType", + "ParameterValue": "FARGATE" + }, + { + "ParameterKey": "HealthCheckUrl", + "ParameterValue": "/health" + }, + { + "ParameterKey": "UseLbForService", + "ParameterValue": "false" + }, + { + "ParameterKey": "InstanceType", + "ParameterValue": "t3a.small" + }, + { + "ParameterKey": "ECSAMI", + "ParameterValue": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + }, + { + "ParameterKey": "ImageUrl", + "ParameterValue": "postgres:15-alpine" + }, + { + "ParameterKey": "TaskDefIncludesFile", + "ParameterValue": "s3://doenet-cf-dev/dev3-doenet/dev3-umami-db-taskdef-includes.yml" + }, + { + "ParameterKey": "TaskRoleIncludesFile", + "ParameterValue": "s3://doenet-cf-dev/dev3-doenet/service-taskrole-includes.yml" + }, + { + "ParameterKey": "ContainerPort", + "ParameterValue": "5432" + }, + { + "ParameterKey": "ContainerMemory", + "ParameterValue": "1024" + }, + { + "ParameterKey": "ContainerCpu", + "ParameterValue": "512" + }, + { + "ParameterKey": "HealthCheckGracePeriod", + "ParameterValue": "300" + }, + { + "ParameterKey": "MaxCount", + "ParameterValue": "1" + }, + { + "ParameterKey": "MinCount", + "ParameterValue": "1" + }, + { + "ParameterKey": "SecurityGroup", + "ParameterValue": "/dev3/EcsInstanceSecurityGroup" + }, + { + "ParameterKey": "PrivateSubnets", + "ParameterValue": "/dev3/PrivateSubnets" + }, + { + "ParameterKey": "VPC", + "ParameterValue": "/dev3/VPC" + }, + { + "ParameterKey": "EnvironmentKmsKey", + "ParameterValue": "/dev3/EnvironmentKmsKey" + }, + { + "ParameterKey": "EcsCluster", + "ParameterValue": "/dev3/EcsCluster" + }, + { + "ParameterKey": "LoadBalancerListener", + "ParameterValue": "/dev3/LoadBalancerHttpsListener" + }, + { + "ParameterKey": "EfsFileSystemId", + "ParameterValue": "/dev3/EfsFileSystemId" + }, + { + "ParameterKey": "PublicHostedZoneId", + "ParameterValue": "/dev3/PublicHostedZoneId" + }, + { + "ParameterKey": "PublicHostedZoneName", + "ParameterValue": "/dev3/PublicHostedZoneName" + }, + { + "ParameterKey": "LoadBalancerDnsName", + "ParameterValue": "/dev3/LoadBalancerDnsName" + }, + { + "ParameterKey": "LoadBalancerHostedZoneId", + "ParameterValue": "/dev3/LoadBalancerHostedZoneId" + }, + { + "ParameterKey": "RestrictedCIDRs", + "ParameterValue": "notapplicable" + }, + { + "ParameterKey": "PathPattern", + "ParameterValue": "notapplicable" + } +] diff --git a/infra/cloudformation/dev3-doenet-umami.params b/infra/cloudformation/dev3-doenet-umami.params new file mode 100644 index 000000000..8264f1caf --- /dev/null +++ b/infra/cloudformation/dev3-doenet-umami.params @@ -0,0 +1,138 @@ +[ + { + "ParameterKey": "RedshiftSecretArn", + "ParameterValue": "notapplicable" + }, + { + "ParameterKey": "DomainNames", + "ParameterValue": "umami.dev3.doenet.org" + }, + { + "ParameterKey": "EnvironmentName", + "ParameterValue": "dev3" + }, + { + "ParameterKey": "ServiceName", + "ParameterValue": "umami" + }, + { + "ParameterKey": "S3AppBucket", + "ParameterValue": "/dev3/S3Bucket" + }, + { + "ParameterKey": "CloudMapPrivateNamespace", + "ParameterValue": "/dev3/CloudMapPrivateNamespace" + }, + { + "ParameterKey": "Priority", + "ParameterValue": "1" + }, + { + "ParameterKey": "EcsLaunchType", + "ParameterValue": "FARGATE" + }, + { + "ParameterKey": "HealthCheckUrl", + "ParameterValue": "/api/heartbeat" + }, + { + "ParameterKey": "UseLbForService", + "ParameterValue": "true" + }, + { + "ParameterKey": "InstanceType", + "ParameterValue": "t3a.small" + }, + { + "ParameterKey": "ECSAMI", + "ParameterValue": "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" + }, + { + "ParameterKey": "ImageUrl", + "ParameterValue": "ghcr.io/umami-software/umami:latest" + }, + { + "ParameterKey": "TaskDefIncludesFile", + "ParameterValue": "s3://doenet-cf-dev/dev3-doenet/dev3-umami-taskdef-includes.yml" + }, + { + "ParameterKey": "TaskRoleIncludesFile", + "ParameterValue": "s3://doenet-cf-dev/dev3-doenet/service-taskrole-includes.yml" + }, + { + "ParameterKey": "ContainerPort", + "ParameterValue": "3000" + }, + { + "ParameterKey": "ContainerMemory", + "ParameterValue": "1024" + }, + { + "ParameterKey": "ContainerCpu", + "ParameterValue": "512" + }, + { + "ParameterKey": "HealthCheckGracePeriod", + "ParameterValue": "300" + }, + { + "ParameterKey": "MaxCount", + "ParameterValue": "1" + }, + { + "ParameterKey": "MinCount", + "ParameterValue": "1" + }, + { + "ParameterKey": "SecurityGroup", + "ParameterValue": "/dev3/EcsInstanceSecurityGroup" + }, + { + "ParameterKey": "PrivateSubnets", + "ParameterValue": "/dev3/PrivateSubnets" + }, + { + "ParameterKey": "VPC", + "ParameterValue": "/dev3/VPC" + }, + { + "ParameterKey": "EnvironmentKmsKey", + "ParameterValue": "/dev3/EnvironmentKmsKey" + }, + { + "ParameterKey": "EcsCluster", + "ParameterValue": "/dev3/EcsCluster" + }, + { + "ParameterKey": "LoadBalancerListener", + "ParameterValue": "/dev3/LoadBalancerHttpsListener" + }, + { + "ParameterKey": "EfsFileSystemId", + "ParameterValue": "/dev3/EfsFileSystemId" + }, + { + "ParameterKey": "PublicHostedZoneId", + "ParameterValue": "/dev3/PublicHostedZoneId" + }, + { + "ParameterKey": "PublicHostedZoneName", + "ParameterValue": "/dev3/PublicHostedZoneName" + }, + { + "ParameterKey": "LoadBalancerDnsName", + "ParameterValue": "/dev3/LoadBalancerDnsName" + }, + { + "ParameterKey": "LoadBalancerHostedZoneId", + "ParameterValue": "/dev3/LoadBalancerHostedZoneId" + }, + { + "ParameterKey": "RestrictedCIDRs", + "ParameterValue": "notapplicable" + }, + { + "ParameterKey": "PathPattern", + "ParameterValue": "notapplicable" + } +] diff --git a/infra/cloudformation/dev3-doenet.params b/infra/cloudformation/dev3-doenet.params index 06adbe459..15ab23dfa 100644 --- a/infra/cloudformation/dev3-doenet.params +++ b/infra/cloudformation/dev3-doenet.params @@ -25,7 +25,7 @@ }, { "ParameterKey": "Priority", - "ParameterValue": "1" + "ParameterValue": "10" }, { "ParameterKey": "EcsLaunchType", diff --git a/infra/cloudformation/dev3-umami-db-taskdef-includes.yml b/infra/cloudformation/dev3-umami-db-taskdef-includes.yml new file mode 100644 index 000000000..dfb8d1e4b --- /dev/null +++ b/infra/cloudformation/dev3-umami-db-taskdef-includes.yml @@ -0,0 +1,20 @@ +MountPoints: + - SourceVolume: efs-share + ContainerPath: /var/lib/postgresql/data +Environment: + - Name: POSTGRES_DB + Value: "umami" + - Name: POSTGRES_USER + Value: "umami" +HealthCheck: + Command: + - CMD-SHELL + - pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB" + Interval: 30 + Timeout: 5 + Retries: 5 + StartPeriod: 30 +Secrets: + - Name: POSTGRES_PASSWORD + ValueFrom: + Fn::Sub: "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/umami/DBPassword" diff --git a/infra/cloudformation/dev3-umami-taskdef-includes.yml b/infra/cloudformation/dev3-umami-taskdef-includes.yml new file mode 100644 index 000000000..d21273fea --- /dev/null +++ b/infra/cloudformation/dev3-umami-taskdef-includes.yml @@ -0,0 +1,10 @@ +Environment: + - Name: PORT + Value: "3000" +Secrets: + - Name: DATABASE_URL + ValueFrom: + Fn::Sub: "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/umami/DatabaseUrl" + - Name: APP_SECRET + ValueFrom: + Fn::Sub: "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvironmentName}/umami/AppSecret" diff --git a/infra/dev3.aws b/infra/dev3.aws index 9a5cbbc00..5d01a955c 100644 --- a/infra/dev3.aws +++ b/infra/dev3.aws @@ -10,6 +10,8 @@ STACKS['dev3-doenet-networking']='networking' STACKS['dev3-doenet-repositories']='repositories' STACKS['dev3-doenet-common']='service-common' STACKS['dev3-doenet-mysql']='service' +STACKS['dev3-doenet-umami-db']='service' +STACKS['dev3-doenet-umami']='service' STACKS['dev3-doenet']='service' STACKS['dev3-doenet-frontend']='s3-hosted-stackset' STACKS['dev3-discourse-forums']='discourse-forums-ec2' @@ -21,6 +23,8 @@ STACK_ORDER=( dev3-doenet-repositories dev3-doenet-common dev3-doenet-mysql + dev3-doenet-umami-db + dev3-doenet-umami dev3-doenet dev3-doenet-frontend )