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
)