diff --git a/.deploy/api/Dockerfile b/.deploy/api/Dockerfile index fac9063068c..e961d325e39 100644 --- a/.deploy/api/Dockerfile +++ b/.deploy/api/Dockerfile @@ -65,6 +65,11 @@ ARG UNLEASH_API_KEY ARG PM2_PUBLIC_KEY ARG PM2_SECRET_KEY ARG PM2_MACHINE_NAME +ARG JITSU_SERVER_URL +ARG JITSU_SERVER_WRITE_KEY +ARG GAUZY_GITHUB_CLIENT_ID +ARG GAUZY_GITHUB_WEBHOOK_URL +ARG GAUZY_GITHUB_WEBHOOK_SECRET FROM node:18-alpine3.17 AS dependencies @@ -291,6 +296,11 @@ ENV UNLEASH_API_KEY=${UNLEASH_API_KEY} ENV PM2_PUBLIC_KEY=${PM2_PUBLIC_KEY} ENV PM2_SECRET_KEY=${PM2_SECRET_KEY} ENV PM2_MACHINE_NAME=${PM2_MACHINE_NAME} +ENV JITSU_SERVER_URL=${JITSU_SERVER_URL} +ENV JITSU_SERVER_WRITE_KEY=${JITSU_SERVER_WRITE_KEY} +ENV GAUZY_GITHUB_CLIENT_ID=${GAUZY_GITHUB_CLIENT_ID} +ENV GAUZY_GITHUB_WEBHOOK_URL=${GAUZY_GITHUB_WEBHOOK_URL} +ENV GAUZY_GITHUB_WEBHOOK_SECRET=${GAUZY_GITHUB_WEBHOOK_SECRET} EXPOSE ${API_PORT} diff --git a/.deploy/k8s/k8s-manifest.civo.demo.yaml b/.deploy/k8s/k8s-manifest.civo.demo.yaml index 11d3e35f27e..95f953addd6 100644 --- a/.deploy/k8s/k8s-manifest.civo.demo.yaml +++ b/.deploy/k8s/k8s-manifest.civo.demo.yaml @@ -117,6 +117,20 @@ spec: value: '23.3332736' - name: DEFAULT_CURRENCY value: 'USD' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_APP_NAME + value: '$GAUZY_GITHUB_APP_NAME' + - name: GAUZY_GITHUB_REDIRECT_URL + value: '$GAUZY_GITHUB_REDIRECT_URL' + - name: GAUZY_GITHUB_POST_INSTALL_URL + value: '$GAUZY_GITHUB_POST_INSTALL_URL' + - name: GAUZY_GITHUB_APP_ID + value: '$GAUZY_GITHUB_APP_ID' + - name: JITSU_BROWSER_URL + value: '$JITSU_BROWSER_URL' + - name: JITSU_BROWSER_WRITE_KEY + value: '$JITSU_BROWSER_WRITE_KEY' ports: - containerPort: 4200 protocol: TCP diff --git a/.deploy/k8s/k8s-manifest.cw.demo.yaml b/.deploy/k8s/k8s-manifest.cw.demo.yaml index dbd27291f2b..6248e08e880 100644 --- a/.deploy/k8s/k8s-manifest.cw.demo.yaml +++ b/.deploy/k8s/k8s-manifest.cw.demo.yaml @@ -154,6 +154,21 @@ spec: value: '23.3332736' - name: DEFAULT_CURRENCY value: 'USD' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_APP_NAME + value: '$GAUZY_GITHUB_APP_NAME' + - name: GAUZY_GITHUB_REDIRECT_URL + value: '$GAUZY_GITHUB_REDIRECT_URL' + - name: GAUZY_GITHUB_POST_INSTALL_URL + value: '$GAUZY_GITHUB_POST_INSTALL_URL' + - name: GAUZY_GITHUB_APP_ID + value: '$GAUZY_GITHUB_APP_ID' + - name: JITSU_BROWSER_URL + value: '$JITSU_BROWSER_URL' + - name: JITSU_BROWSER_WRITE_KEY + value: '$JITSU_BROWSER_WRITE_KEY' + ports: - containerPort: 4200 protocol: TCP diff --git a/.deploy/k8s/k8s-manifest.cw.prod.yaml b/.deploy/k8s/k8s-manifest.cw.prod.yaml index 69a94b153c1..1191f24fd63 100644 --- a/.deploy/k8s/k8s-manifest.cw.prod.yaml +++ b/.deploy/k8s/k8s-manifest.cw.prod.yaml @@ -195,6 +195,16 @@ spec: value: '$PM2_SECRET_KEY' - name: PM2_MACHINE_NAME value: '$PM2_MACHINE_NAME' + - name: JITSU_SERVER_URL + value: '$JITSU_SERVER_URL' + - name: JITSU_SERVER_WRITE_KEY + value: '$JITSU_SERVER_WRITE_KEY' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_WEBHOOK_URL + value: '$GAUZY_GITHUB_WEBHOOK_URL' + - name: GAUZY_GITHUB_WEBHOOK_SECRET + value: '$GAUZY_GITHUB_WEBHOOK_SECRET' ports: - containerPort: 3000 @@ -275,6 +285,21 @@ spec: value: '23.3332736' - name: DEFAULT_CURRENCY value: 'USD' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_APP_NAME + value: '$GAUZY_GITHUB_APP_NAME' + - name: GAUZY_GITHUB_REDIRECT_URL + value: '$GAUZY_GITHUB_REDIRECT_URL' + - name: GAUZY_GITHUB_POST_INSTALL_URL + value: '$GAUZY_GITHUB_POST_INSTALL_URL' + - name: GAUZY_GITHUB_APP_ID + value: '$GAUZY_GITHUB_APP_ID' + - name: JITSU_BROWSER_URL + value: '$JITSU_BROWSER_URL' + - name: JITSU_BROWSER_WRITE_KEY + value: '$JITSU_BROWSER_WRITE_KEY' + ports: - containerPort: 4200 protocol: TCP diff --git a/.deploy/k8s/k8s-manifest.cw.stage.yaml b/.deploy/k8s/k8s-manifest.cw.stage.yaml index b3162f2993f..721c063f092 100644 --- a/.deploy/k8s/k8s-manifest.cw.stage.yaml +++ b/.deploy/k8s/k8s-manifest.cw.stage.yaml @@ -189,6 +189,16 @@ spec: value: '$UNLEASH_METRICS_INTERVAL' - name: UNLEASH_API_KEY value: '$UNLEASH_API_KEY' + - name: JITSU_SERVER_URL + value: '$JITSU_SERVER_URL' + - name: JITSU_SERVER_WRITE_KEY + value: '$JITSU_SERVER_WRITE_KEY' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_WEBHOOK_URL + value: '$GAUZY_GITHUB_WEBHOOK_URL' + - name: GAUZY_GITHUB_WEBHOOK_SECRET + value: '$GAUZY_GITHUB_WEBHOOK_SECRET' ports: - containerPort: 3000 @@ -269,6 +279,21 @@ spec: value: '23.3332736' - name: DEFAULT_CURRENCY value: 'USD' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_APP_NAME + value: '$GAUZY_GITHUB_APP_NAME' + - name: GAUZY_GITHUB_REDIRECT_URL + value: '$GAUZY_GITHUB_REDIRECT_URL' + - name: GAUZY_GITHUB_POST_INSTALL_URL + value: '$GAUZY_GITHUB_POST_INSTALL_URL' + - name: GAUZY_GITHUB_APP_ID + value: '$GAUZY_GITHUB_APP_ID' + - name: JITSU_BROWSER_URL + value: '$JITSU_BROWSER_URL' + - name: JITSU_BROWSER_WRITE_KEY + value: '$JITSU_BROWSER_WRITE_KEY' + ports: - containerPort: 4200 protocol: TCP diff --git a/.deploy/k8s/k8s-manifest.demo.yaml b/.deploy/k8s/k8s-manifest.demo.yaml index 0ea322b6a83..36bcfda167c 100644 --- a/.deploy/k8s/k8s-manifest.demo.yaml +++ b/.deploy/k8s/k8s-manifest.demo.yaml @@ -135,6 +135,20 @@ spec: value: '23.3332736' - name: DEFAULT_CURRENCY value: 'USD' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_APP_NAME + value: '$GAUZY_GITHUB_APP_NAME' + - name: GAUZY_GITHUB_REDIRECT_URL + value: '$GAUZY_GITHUB_REDIRECT_URL' + - name: GAUZY_GITHUB_POST_INSTALL_URL + value: '$GAUZY_GITHUB_POST_INSTALL_URL' + - name: GAUZY_GITHUB_APP_ID + value: '$GAUZY_GITHUB_APP_ID' + - name: JITSU_BROWSER_URL + value: '$JITSU_BROWSER_URL' + - name: JITSU_BROWSER_WRITE_KEY + value: '$JITSU_BROWSER_WRITE_KEY' ports: - containerPort: 4200 diff --git a/.deploy/k8s/k8s-manifest.prod.yaml b/.deploy/k8s/k8s-manifest.prod.yaml index 99fd72448a3..d0840bff011 100644 --- a/.deploy/k8s/k8s-manifest.prod.yaml +++ b/.deploy/k8s/k8s-manifest.prod.yaml @@ -187,6 +187,16 @@ spec: value: '$PM2_SECRET_KEY' - name: PM2_MACHINE_NAME value: '$PM2_MACHINE_NAME' + - name: JITSU_SERVER_URL + value: '$JITSU_SERVER_URL' + - name: JITSU_SERVER_WRITE_KEY + value: '$JITSU_SERVER_WRITE_KEY' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_WEBHOOK_URL + value: '$GAUZY_GITHUB_WEBHOOK_URL' + - name: GAUZY_GITHUB_WEBHOOK_SECRET + value: '$GAUZY_GITHUB_WEBHOOK_SECRET' ports: - containerPort: 3000 @@ -234,6 +244,21 @@ spec: value: '23.3332736' - name: DEFAULT_CURRENCY value: 'USD' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_APP_NAME + value: '$GAUZY_GITHUB_APP_NAME' + - name: GAUZY_GITHUB_REDIRECT_URL + value: '$GAUZY_GITHUB_REDIRECT_URL' + - name: GAUZY_GITHUB_POST_INSTALL_URL + value: '$GAUZY_GITHUB_POST_INSTALL_URL' + - name: GAUZY_GITHUB_APP_ID + value: '$GAUZY_GITHUB_APP_ID' + - name: JITSU_BROWSER_URL + value: '$JITSU_BROWSER_URL' + - name: JITSU_BROWSER_WRITE_KEY + value: '$JITSU_BROWSER_WRITE_KEY' + ports: - containerPort: 4200 protocol: TCP diff --git a/.deploy/k8s/k8s-manifest.stage.yaml b/.deploy/k8s/k8s-manifest.stage.yaml index 148885262f1..7999861c8f1 100644 --- a/.deploy/k8s/k8s-manifest.stage.yaml +++ b/.deploy/k8s/k8s-manifest.stage.yaml @@ -181,6 +181,16 @@ spec: value: '$UNLEASH_METRICS_INTERVAL' - name: UNLEASH_API_KEY value: '$UNLEASH_API_KEY' + - name: JITSU_SERVER_URL + value: '$JITSU_SERVER_URL' + - name: JITSU_SERVER_WRITE_KEY + value: '$JITSU_SERVER_WRITE_KEY' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_WEBHOOK_URL + value: '$GAUZY_GITHUB_WEBHOOK_URL' + - name: GAUZY_GITHUB_WEBHOOK_SECRET + value: '$GAUZY_GITHUB_WEBHOOK_SECRET' ports: - containerPort: 3000 @@ -228,6 +238,21 @@ spec: value: '23.3332736' - name: DEFAULT_CURRENCY value: 'USD' + - name: GAUZY_GITHUB_CLIENT_ID + value: '$GAUZY_GITHUB_CLIENT_ID' + - name: GAUZY_GITHUB_APP_NAME + value: '$GAUZY_GITHUB_APP_NAME' + - name: GAUZY_GITHUB_REDIRECT_URL + value: '$GAUZY_GITHUB_REDIRECT_URL' + - name: GAUZY_GITHUB_POST_INSTALL_URL + value: '$GAUZY_GITHUB_POST_INSTALL_URL' + - name: GAUZY_GITHUB_APP_ID + value: '$GAUZY_GITHUB_APP_ID' + - name: JITSU_BROWSER_URL + value: '$JITSU_BROWSER_URL' + - name: JITSU_BROWSER_WRITE_KEY + value: '$JITSU_BROWSER_WRITE_KEY' + ports: - containerPort: 4200 protocol: TCP diff --git a/.deploy/webapp/Dockerfile b/.deploy/webapp/Dockerfile index f050805535b..42b8be601a4 100644 --- a/.deploy/webapp/Dockerfile +++ b/.deploy/webapp/Dockerfile @@ -23,6 +23,13 @@ ARG DEFAULT_COUNTRY ARG DEMO ARG WEB_HOST ARG WEB_PORT +ARG GAUZY_GITHUB_CLIENT_ID +ARG GAUZY_GITHUB_APP_NAME +ARG GAUZY_GITHUB_REDIRECT_URL +ARG GAUZY_GITHUB_POST_INSTALL_URL +ARG GAUZY_GITHUB_APP_ID +ARG JITSU_BROWSER_URL +ARG JITSU_BROWSER_WRITE_KEY FROM node:18-alpine3.17 AS dependencies @@ -135,6 +142,13 @@ ENV DEFAULT_COUNTRY=${DEFAULT_COUNTRY} ENV GAUZY_CLOUD_APP=${GAUZY_CLOUD_APP} ENV CHAT_MESSAGE_GOOGLE_MAP=${CHAT_MESSAGE_GOOGLE_MAP} ENV HUBSTAFF_REDIRECT_URL=${HUBSTAFF_REDIRECT_URL} +ENV GAUZY_GITHUB_CLIENT_ID=${GAUZY_GITHUB_CLIENT_ID} +ENV GAUZY_GITHUB_APP_NAME=${GAUZY_GITHUB_APP_NAME} +ENV GAUZY_GITHUB_REDIRECT_URL=${GAUZY_GITHUB_REDIRECT_URL} +ENV GAUZY_GITHUB_APP_ID=${GAUZY_GITHUB_APP_ID} +ENV GAUZY_GITHUB_POST_INSTALL_URL=${GAUZY_GITHUB_POST_INSTALL_URL} +ENV JITSU_BROWSER_URL=${JITSU_BROWSER_URL} +ENV JITSU_BROWSER_WRITE_KEY=${JITSU_BROWSER_WRITE_KEY} EXPOSE ${WEB_PORT} diff --git a/.deploy/webapp/entrypoint.compose.sh b/.deploy/webapp/entrypoint.compose.sh index e553fe3109f..4d01f8f02af 100644 --- a/.deploy/webapp/entrypoint.compose.sh +++ b/.deploy/webapp/entrypoint.compose.sh @@ -27,7 +27,15 @@ sed -i "s#DOCKER_DEFAULT_COUNTRY#$DEFAULT_COUNTRY#g" *.js sed -i "s#DOCKER_DEMO#$DEMO#g" *.js sed -i "s#DOCKER_WEB_HOST#$WEB_HOST#g" *.js sed -i "s#DOCKER_WEB_PORT#$WEB_PORT#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_CLIENT_ID#$GAUZY_GITHUB_CLIENT_ID#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_APP_NAME#$GAUZY_GITHUB_APP_NAME#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_REDIRECT_URL#$GAUZY_GITHUB_REDIRECT_URL#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_APP_ID#$GAUZY_GITHUB_APP_ID#g" *.js +sed -i "s|DOCKER_GAUZY_GITHUB_POST_INSTALL_URL|$GAUZY_GITHUB_POST_INSTALL_URL|g" *.js +sed -i "s#DOCKER_JITSU_BROWSER_URL#$JITSU_BROWSER_URL#g" *.js +sed -i "s#DOCKER_JITSU_BROWSER_WRITE_KEY#$JITSU_BROWSER_WRITE_KEY#g" *.js +# We need to copy nginx.conf to correct place envsubst '${API_HOST} ${API_PORT}' < /etc/nginx/conf.d/compose.conf.template > /etc/nginx/nginx.conf # In Docker Compose we should wait other services start diff --git a/.deploy/webapp/entrypoint.prod.sh b/.deploy/webapp/entrypoint.prod.sh index 6de43e5601b..0663b5eece1 100755 --- a/.deploy/webapp/entrypoint.prod.sh +++ b/.deploy/webapp/entrypoint.prod.sh @@ -25,6 +25,13 @@ sed -i "s#DOCKER_DEFAULT_COUNTRY#$DEFAULT_COUNTRY#g" *.js sed -i "s#DOCKER_DEMO#$DEMO#g" *.js sed -i "s#DOCKER_WEB_HOST#$WEB_HOST#g" *.js sed -i "s#DOCKER_WEB_PORT#$WEB_PORT#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_CLIENT_ID#$GAUZY_GITHUB_CLIENT_ID#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_APP_NAME#$GAUZY_GITHUB_APP_NAME#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_REDIRECT_URL#$GAUZY_GITHUB_REDIRECT_URL#g" *.js +sed -i "s#DOCKER_GAUZY_GITHUB_APP_ID#$GAUZY_GITHUB_APP_ID#g" *.js +sed -i "s|DOCKER_GAUZY_GITHUB_POST_INSTALL_URL|$GAUZY_GITHUB_POST_INSTALL_URL|g" *.js +sed -i "s#DOCKER_JITSU_BROWSER_URL#$JITSU_BROWSER_URL#g" *.js +sed -i "s#DOCKER_JITSU_BROWSER_WRITE_KEY#$JITSU_BROWSER_WRITE_KEY#g" *.js # We may not need to use that env vars now in nginx.config, but we may want later. # Also we just need to copy nginx.conf to correct place anyway... diff --git a/.env.compose b/.env.compose index ac0974c0268..a57da7da22a 100644 --- a/.env.compose +++ b/.env.compose @@ -309,6 +309,12 @@ HUBSTAFF_CLIENT_ID= HUBSTAFF_CLIENT_SECRET= HUBSTAFF_PERSONAL_ACCESS_TOKEN= -# Jitsu Configuration -JITSU_BROWSER_HOST= +# Jitsu Browser Configuration +JITSU_BROWSER_URL= JITSU_BROWSER_WRITE_KEY= + +# Jitsu Server Configuration +JITSU_SERVER_URL= +JITSU_SERVER_WRITE_KEY= +JITSU_SERVER_DEBUG= +JITSU_SERVER_ECHO_EVENTS= diff --git a/.env.docker b/.env.docker index f1472334373..12cb10ba211 100644 --- a/.env.docker +++ b/.env.docker @@ -282,6 +282,12 @@ HUBSTAFF_CLIENT_ID= HUBSTAFF_CLIENT_SECRET= HUBSTAFF_PERSONAL_ACCESS_TOKEN= -# Jitsu Configuration -JITSU_BROWSER_HOST= +# Jitsu Browser Configuration +JITSU_BROWSER_URL= JITSU_BROWSER_WRITE_KEY= + +# Jitsu Server Configuration +JITSU_SERVER_URL= +JITSU_SERVER_WRITE_KEY= +JITSU_SERVER_DEBUG= +JITSU_SERVER_ECHO_EVENTS= diff --git a/.env.local b/.env.local index 85f205cb086..ec2482079a2 100644 --- a/.env.local +++ b/.env.local @@ -269,6 +269,12 @@ HUBSTAFF_CLIENT_ID= HUBSTAFF_CLIENT_SECRET= HUBSTAFF_PERSONAL_ACCESS_TOKEN= -# Jitsu Configuration -JITSU_BROWSER_HOST= +# Jitsu Browser Configuration +JITSU_BROWSER_URL= JITSU_BROWSER_WRITE_KEY= + +# Jitsu Server Configuration +JITSU_SERVER_URL= +JITSU_SERVER_WRITE_KEY= +JITSU_SERVER_DEBUG= +JITSU_SERVER_ECHO_EVENTS= diff --git a/.env.sample b/.env.sample index e56138c1118..449079d46f0 100644 --- a/.env.sample +++ b/.env.sample @@ -302,6 +302,12 @@ HUBSTAFF_CLIENT_ID= HUBSTAFF_CLIENT_SECRET= HUBSTAFF_PERSONAL_ACCESS_TOKEN= -# Jitsu Configuration -JITSU_BROWSER_HOST= +# Jitsu Browser Configuration +JITSU_BROWSER_URL= JITSU_BROWSER_WRITE_KEY= + +# Jitsu Server Configuration +JITSU_SERVER_URL= +JITSU_SERVER_WRITE_KEY= +JITSU_SERVER_DEBUG= +JITSU_SERVER_ECHO_EVENTS= diff --git a/.github/workflows/deploy-civo-demo.yml b/.github/workflows/deploy-civo-demo.yml index 2f0ee8834cc..c8a17fb1d1b 100644 --- a/.github/workflows/deploy-civo-demo.yml +++ b/.github/workflows/deploy-civo-demo.yml @@ -36,6 +36,7 @@ jobs: run: | envsubst < $GITHUB_WORKSPACE/.deploy/k8s/k8s-manifest.civo.demo.yaml | kubectl --context ever apply -f - env: + # below we are using GitHub secrets for both frontend and backend DB_NAME: 'gauzy_demo' SENTRY_DSN: '${{ secrets.SENTRY_DSN }}' SENTRY_TRACES_SAMPLE_RATE: '${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}' diff --git a/.github/workflows/deploy-cw-demo.yml b/.github/workflows/deploy-cw-demo.yml index 875c1ae4207..5de7ec54b06 100644 --- a/.github/workflows/deploy-cw-demo.yml +++ b/.github/workflows/deploy-cw-demo.yml @@ -36,6 +36,7 @@ jobs: run: | envsubst < $GITHUB_WORKSPACE/.deploy/k8s/k8s-manifest.cw.demo.yaml | kubectl --context coreweave apply -f - env: + # below we are using GitHub secrets for both frontend and backend DB_NAME: 'gauzy_demo' SENTRY_DSN: '${{ secrets.SENTRY_DSN }}' SENTRY_TRACES_SAMPLE_RATE: '${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}' diff --git a/.github/workflows/deploy-cw-prod.yml b/.github/workflows/deploy-cw-prod.yml index 3ec7d02c0e4..b4603edc34b 100644 --- a/.github/workflows/deploy-cw-prod.yml +++ b/.github/workflows/deploy-cw-prod.yml @@ -42,6 +42,7 @@ jobs: run: | envsubst < $GITHUB_WORKSPACE/.deploy/k8s/k8s-manifest.cw.prod.yaml | kubectl --context coreweave apply -f - env: + # below we are using GitHub secrets for both frontend and backend DB_TYPE: '${{ secrets.DB_TYPE }}' DB_URI: '${{ secrets.DB_URI }}' # Note: for now we are using DB in different provider, so we have to use public hostname @@ -99,6 +100,17 @@ jobs: PM2_MACHINE_NAME: '${{ secrets.PM2_MACHINE_NAME }}' PM2_SECRET_KEY: '${{ secrets.PM2_SECRET_KEY }}' PM2_PUBLIC_KEY: '${{ secrets.PM2_PUBLIC_KEY }}' + JITSU_SERVER_URL: '${{ secrets.JITSU_SERVER_URL }}' + JITSU_SERVER_WRITE_KEY: '${{ secrets.JITSU_SERVER_WRITE_KEY }}' + GAUZY_GITHUB_CLIENT_ID: '${{ secrets.GAUZY_GITHUB_CLIENT_ID }}' + GAUZY_GITHUB_WEBHOOK_URL: '${{ secrets.GAUZY_GITHUB_WEBHOOK_URL }}' + GAUZY_GITHUB_WEBHOOK_SECRET: '${{ secrets.GAUZY_GITHUB_WEBHOOK_SECRET }}' + GAUZY_GITHUB_APP_NAME: '${{ secrets.GAUZY_GITHUB_APP_NAME }}' + GAUZY_GITHUB_REDIRECT_URL: '${{ secrets.GAUZY_GITHUB_REDIRECT_URL }}' + GAUZY_GITHUB_POST_INSTALL_URL: '${{ secrets.GAUZY_GITHUB_POST_INSTALL_URL }}' + GAUZY_GITHUB_APP_ID: '${{ secrets.GAUZY_GITHUB_APP_ID }}' + JITSU_BROWSER_URL: '${{ secrets.JITSU_BROWSER_URL }}' + JITSU_BROWSER_WRITE_KEY: '${{ secrets.JITSU_BROWSER_WRITE_KEY }}' # we need this step because for now we just use :latest tag # note: for production we will use different strategy later diff --git a/.github/workflows/deploy-cw-stage.yml b/.github/workflows/deploy-cw-stage.yml index 1280882496c..2f102d8fc11 100644 --- a/.github/workflows/deploy-cw-stage.yml +++ b/.github/workflows/deploy-cw-stage.yml @@ -42,6 +42,7 @@ jobs: run: | envsubst < $GITHUB_WORKSPACE/.deploy/k8s/k8s-manifest.cw.stage.yaml | kubectl --context coreweave apply -f - env: + # below we are using GitHub secrets for both frontend and backend DB_TYPE: '${{ secrets.DB_TYPE }}' DB_URI: '${{ secrets.DB_URI }}' # Note: for now we are using DB in different provider, so we have to use public hostname @@ -100,6 +101,17 @@ jobs: PM2_MACHINE_NAME: '${{ secrets.PM2_MACHINE_NAME }}' PM2_SECRET_KEY: '${{ secrets.PM2_SECRET_KEY }}' PM2_PUBLIC_KEY: '${{ secrets.PM2_PUBLIC_KEY }}' + JITSU_SERVER_URL: '${{ secrets.JITSU_SERVER_URL }}' + JITSU_SERVER_WRITE_KEY: '${{ secrets.JITSU_SERVER_WRITE_KEY }}' + GAUZY_GITHUB_CLIENT_ID: '${{ secrets.GAUZY_GITHUB_CLIENT_ID }}' + GAUZY_GITHUB_WEBHOOK_URL: '${{ secrets.GAUZY_GITHUB_WEBHOOK_URL }}' + GAUZY_GITHUB_WEBHOOK_SECRET: '${{ secrets.GAUZY_GITHUB_WEBHOOK_SECRET }}' + GAUZY_GITHUB_APP_NAME: '${{ secrets.GAUZY_GITHUB_APP_NAME }}' + GAUZY_GITHUB_REDIRECT_URL: '${{ secrets.GAUZY_GITHUB_REDIRECT_URL }}' + GAUZY_GITHUB_POST_INSTALL_URL: '${{ secrets.GAUZY_GITHUB_POST_INSTALL_URL }}' + GAUZY_GITHUB_APP_ID: '${{ secrets.GAUZY_GITHUB_APP_ID }}' + JITSU_BROWSER_URL: '${{ secrets.JITSU_BROWSER_URL }}' + JITSU_BROWSER_WRITE_KEY: '${{ secrets.JITSU_BROWSER_WRITE_KEY }}' # we need this step because for now we just use :latest tag # note: for production we will use different strategy later diff --git a/.github/workflows/deploy-do-demo.yml b/.github/workflows/deploy-do-demo.yml index 969e1f6178e..f571800ba99 100644 --- a/.github/workflows/deploy-do-demo.yml +++ b/.github/workflows/deploy-do-demo.yml @@ -32,6 +32,7 @@ jobs: run: | envsubst < $GITHUB_WORKSPACE/.deploy/k8s/k8s-manifest.demo.yaml | kubectl --context do-sfo2-k8s-gauzy apply -f - env: + # below we are using GitHub secrets for both frontend and backend DB_NAME: 'gauzy_demo' SENTRY_DSN: '${{ secrets.SENTRY_DSN }}' SENTRY_TRACES_SAMPLE_RATE: '${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}' diff --git a/.github/workflows/deploy-do-prod.yml b/.github/workflows/deploy-do-prod.yml index 05110d0ddfb..f2aea9183c7 100644 --- a/.github/workflows/deploy-do-prod.yml +++ b/.github/workflows/deploy-do-prod.yml @@ -38,6 +38,7 @@ jobs: run: | envsubst < $GITHUB_WORKSPACE/.deploy/k8s/k8s-manifest.prod.yaml | kubectl --context do-sfo2-k8s-gauzy apply -f - env: + # below we are using GitHub secrets for both frontend and backend DB_TYPE: '${{ secrets.DB_TYPE }}' DB_URI: '${{ secrets.DB_URI }}' DB_HOST: '${{ secrets.DB_HOST }}' @@ -94,6 +95,17 @@ jobs: PM2_MACHINE_NAME: '${{ secrets.PM2_MACHINE_NAME }}' PM2_SECRET_KEY: '${{ secrets.PM2_SECRET_KEY }}' PM2_PUBLIC_KEY: '${{ secrets.PM2_PUBLIC_KEY }}' + JITSU_SERVER_URL: '${{ secrets.JITSU_SERVER_URL }}' + JITSU_SERVER_WRITE_KEY: '${{ secrets.JITSU_SERVER_WRITE_KEY }}' + GAUZY_GITHUB_CLIENT_ID: '${{ secrets.GAUZY_GITHUB_CLIENT_ID }}' + GAUZY_GITHUB_WEBHOOK_URL: '${{ secrets.GAUZY_GITHUB_WEBHOOK_URL }}' + GAUZY_GITHUB_WEBHOOK_SECRET: '${{ secrets.GAUZY_GITHUB_WEBHOOK_SECRET }}' + GAUZY_GITHUB_APP_NAME: '${{ secrets.GAUZY_GITHUB_APP_NAME }}' + GAUZY_GITHUB_REDIRECT_URL: '${{ secrets.GAUZY_GITHUB_REDIRECT_URL }}' + GAUZY_GITHUB_POST_INSTALL_URL: '${{ secrets.GAUZY_GITHUB_POST_INSTALL_URL }}' + GAUZY_GITHUB_APP_ID: '${{ secrets.GAUZY_GITHUB_APP_ID }}' + JITSU_BROWSER_URL: '${{ secrets.JITSU_BROWSER_URL }}' + JITSU_BROWSER_WRITE_KEY: '${{ secrets.JITSU_BROWSER_WRITE_KEY }}' # we need this step because for now we just use :latest tag # note: for production we will use different strategy later diff --git a/.github/workflows/deploy-do-stage.yml b/.github/workflows/deploy-do-stage.yml index 99f698739f2..969ca292acb 100644 --- a/.github/workflows/deploy-do-stage.yml +++ b/.github/workflows/deploy-do-stage.yml @@ -38,6 +38,7 @@ jobs: run: | envsubst < $GITHUB_WORKSPACE/.deploy/k8s/k8s-manifest.stage.yaml | kubectl --context do-sfo2-k8s-gauzy apply -f - env: + # below we are using GitHub secrets for both frontend and backend DB_TYPE: '${{ secrets.DB_TYPE }}' DB_URI: '${{ secrets.DB_URI }}' DB_HOST: '${{ secrets.DB_HOST }}' @@ -95,6 +96,17 @@ jobs: PM2_MACHINE_NAME: '${{ secrets.PM2_MACHINE_NAME }}' PM2_SECRET_KEY: '${{ secrets.PM2_SECRET_KEY }}' PM2_PUBLIC_KEY: '${{ secrets.PM2_PUBLIC_KEY }}' + JITSU_SERVER_URL: '${{ secrets.JITSU_SERVER_URL }}' + JITSU_SERVER_WRITE_KEY: '${{ secrets.JITSU_SERVER_WRITE_KEY }}' + GAUZY_GITHUB_CLIENT_ID: '${{ secrets.GAUZY_GITHUB_CLIENT_ID }}' + GAUZY_GITHUB_WEBHOOK_URL: '${{ secrets.GAUZY_GITHUB_WEBHOOK_URL }}' + GAUZY_GITHUB_WEBHOOK_SECRET: '${{ secrets.GAUZY_GITHUB_WEBHOOK_SECRET }}' + GAUZY_GITHUB_APP_NAME: '${{ secrets.GAUZY_GITHUB_APP_NAME }}' + GAUZY_GITHUB_REDIRECT_URL: '${{ secrets.GAUZY_GITHUB_REDIRECT_URL }}' + GAUZY_GITHUB_POST_INSTALL_URL: '${{ secrets.GAUZY_GITHUB_POST_INSTALL_URL }}' + GAUZY_GITHUB_APP_ID: '${{ secrets.GAUZY_GITHUB_APP_ID }}' + JITSU_BROWSER_URL: '${{ secrets.JITSU_BROWSER_URL }}' + JITSU_BROWSER_WRITE_KEY: '${{ secrets.JITSU_BROWSER_WRITE_KEY }}' # we need this step because for now we just use :latest tag # note: for production we will use different strategy later diff --git a/.scripts/configure.ts b/.scripts/configure.ts index 44071d3bf16..e9731c350d7 100644 --- a/.scripts/configure.ts +++ b/.scripts/configure.ts @@ -40,9 +40,9 @@ if (!env.IS_DOCKER) { ); } - if (!env.JITSU_BROWSER_HOST || !env.JITSU_BROWSER_WRITE_KEY) { + if (!env.JITSU_BROWSER_URL || !env.JITSU_BROWSER_WRITE_KEY) { console.warn( - 'WARNING: No Jitsu keys defined in the .env file. Jitsu analytics may not be working!' + 'WARNING: No Jitsu keys defined for browser in the .env file. Jitsu analytics may not be working!' ); } @@ -149,7 +149,7 @@ if (!env.IS_DOCKER) { FILE_PROVIDER: '${env.FILE_PROVIDER}', - JITSU_BROWSER_HOST: '${env.JITSU_BROWSER_HOST}', + JITSU_BROWSER_URL: '${env.JITSU_BROWSER_URL}', JITSU_BROWSER_WRITE_KEY: '${env.JITSU_BROWSER_WRITE_KEY}', GAUZY_GITHUB_APP_NAME: '${env.GAUZY_GITHUB_APP_NAME}', @@ -256,14 +256,14 @@ if (!env.IS_DOCKER) { FILE_PROVIDER: '${env.FILE_PROVIDER}', - JITSU_BROWSER_HOST: '${env.JITSU_BROWSER_HOST}', - JITSU_BROWSER_WRITE_KEY: '${env.JITSU_BROWSER_WRITE_KEY}', + JITSU_BROWSER_URL: 'DOCKER_JITSU_BROWSER_URL', + JITSU_BROWSER_WRITE_KEY: 'DOCKER_JITSU_BROWSER_WRITE_KEY', - GAUZY_GITHUB_APP_NAME: '${env.GAUZY_GITHUB_APP_NAME}', - GAUZY_GITHUB_APP_ID: '${env.GAUZY_GITHUB_APP_ID}', - GAUZY_GITHUB_CLIENT_ID: '${env.GAUZY_GITHUB_CLIENT_ID}', - GAUZY_GITHUB_REDIRECT_URL: '${env.GAUZY_GITHUB_REDIRECT_URL}', - GAUZY_GITHUB_POST_INSTALL_URL: '${env.GAUZY_GITHUB_POST_INSTALL_URL}', + GAUZY_GITHUB_APP_NAME: 'DOCKER_GAUZY_GITHUB_APP_NAME', + GAUZY_GITHUB_APP_ID: 'DOCKER_GAUZY_GITHUB_APP_ID', + GAUZY_GITHUB_CLIENT_ID: 'DOCKER_GAUZY_GITHUB_CLIENT_ID', + GAUZY_GITHUB_REDIRECT_URL: 'DOCKER_GAUZY_GITHUB_REDIRECT_URL', + GAUZY_GITHUB_POST_INSTALL_URL: 'DOCKER_GAUZY_GITHUB_POST_INSTALL_URL', }; `; } @@ -290,10 +290,10 @@ if (!isProd) { // we always want first to remove old generated files (one of them is not needed for current build) try { unlinkSync(`./apps/gauzy/src/environments/environment.ts`); -} catch { } +} catch {} try { unlinkSync(`./apps/gauzy/src/environments/environment.prod.ts`); -} catch { } +} catch {} const envFileDest: string = isProd ? 'environment.prod.ts' : 'environment.ts'; const envFileDestOther: string = !isProd diff --git a/.scripts/env.ts b/.scripts/env.ts index fa8fa38e5d9..c0a59597637 100644 --- a/.scripts/env.ts +++ b/.scripts/env.ts @@ -66,8 +66,8 @@ export type Env = Readonly<{ FILE_PROVIDER: string; - // Jitsu Analytics - JITSU_BROWSER_HOST: string; + // Jitsu Browser Configurations + JITSU_BROWSER_URL: string; JITSU_BROWSER_WRITE_KEY: string; GAUZY_GITHUB_APP_NAME: string; @@ -141,7 +141,7 @@ export const env: Env = cleanEnv( FILE_PROVIDER: str({ default: 'LOCAL' }), - JITSU_BROWSER_HOST: str({ default: '' }), + JITSU_BROWSER_URL: str({ default: '' }), JITSU_BROWSER_WRITE_KEY: str({ default: '' }), GAUZY_GITHUB_APP_NAME: str({ default: '' }), diff --git a/apps/gauzy/src/app/@core/services/analytics/event.type.ts b/apps/gauzy/src/app/@core/services/analytics/event.type.ts index 81ee109837e..d2c08967990 100644 --- a/apps/gauzy/src/app/@core/services/analytics/event.type.ts +++ b/apps/gauzy/src/app/@core/services/analytics/event.type.ts @@ -1,11 +1,11 @@ interface IUserCreatedEvent { - eventType: 'UserCreated'; + eventType: JitsuAnalyticsEventsEnum.USER_CREATED; userId: string; email: string; } interface IButtonClickedEvent { - eventType: 'ButtonClicked'; + eventType: JitsuAnalyticsEventsEnum.BUTTON_CLICKED; url: string; userId: string; userEmail: string; @@ -16,28 +16,28 @@ interface IMenuItemClickedEvent extends IButtonClickedEvent { } interface IPageViewEvent { - eventType: 'PageView'; + eventType: JitsuAnalyticsEventsEnum.PAGE_VIEW; url: string; periodicity: string; } interface IPageCreatedEvent { - eventType: 'PageCreated'; + eventType: JitsuAnalyticsEventsEnum.PAGE_CREATED; slug: string; } interface IUserUpgradedEvent { - eventType: 'UserUpgraded'; + eventType: JitsuAnalyticsEventsEnum.USER_UPGRADED; email: string; } interface IUserClickDownloadAppEvent { - eventType: 'UserClickDownloadApp'; + eventType: JitsuAnalyticsEventsEnum.USER_CLICK_DOWNLOAD_APP; email: string; } interface IUserSignedInEvent { - eventType: 'UserSignedIn'; + eventType: JitsuAnalyticsEventsEnum.USER_SIGNED_IN; email: string; } @@ -54,8 +54,11 @@ type JitsuAnalyticsEvents = export default JitsuAnalyticsEvents; export enum JitsuAnalyticsEventsEnum { - USER_CREATED = 'User Created', - BUTTON_CLICKED = 'Button_Clicked', - PAGE_VIEW = 'Page_View', - PAGE_CREATED = 'Page Created', + USER_CREATED = 'UserCreated', + USER_SIGNED_IN = 'UserSignedIn', + USER_CLICK_DOWNLOAD_APP = 'UserClickDownloadApp', + USER_UPGRADED = 'UserUpgraded', + BUTTON_CLICKED = 'ButtonClicked', + PAGE_VIEW = 'PageView', + PAGE_CREATED = 'PageCreated' } diff --git a/apps/gauzy/src/app/@core/services/analytics/jitsu.service.ts b/apps/gauzy/src/app/@core/services/analytics/jitsu.service.ts index 6afc59090ff..293f587e8a9 100644 --- a/apps/gauzy/src/app/@core/services/analytics/jitsu.service.ts +++ b/apps/gauzy/src/app/@core/services/analytics/jitsu.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { NavigationEnd, Router } from '@angular/router'; -import { jitsuAnalytics, emptyAnalytics, AnalyticsInterface } from '@jitsu/js'; -import { filter } from 'rxjs/operators'; import { Location } from '@angular/common'; +import { filter } from 'rxjs/operators'; +import { jitsuAnalytics, emptyAnalytics, AnalyticsInterface } from '@jitsu/js'; import { environment } from '@env/environment'; import JitsuAnalyticsEvents, { JitsuAnalyticsEventsEnum } from './event.type'; @@ -11,13 +11,18 @@ import JitsuAnalyticsEvents, { JitsuAnalyticsEventsEnum } from './event.type'; }) export class JitsuService { private jitsuClient: AnalyticsInterface; - constructor(private location: Location, private router: Router) { + + constructor( + private readonly location: Location, + private readonly router: Router + ) { this.jitsuClient = - environment.JITSU_BROWSER_HOST && - environment.JITSU_BROWSER_WRITE_KEY + environment.JITSU_BROWSER_URL && environment.JITSU_BROWSER_WRITE_KEY ? jitsuAnalytics({ - host: environment.JITSU_BROWSER_HOST, + host: environment.JITSU_BROWSER_URL, writeKey: environment.JITSU_BROWSER_WRITE_KEY, + debug: false, + echoEvents: false, }) : emptyAnalytics; } @@ -60,7 +65,7 @@ export class JitsuService { }); } - //this deletes the data store + // this deletes the data store async reset(): Promise { return await this.jitsuClient.reset(); } diff --git a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.ts b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.ts index ab58fc686fc..b8f7bde7b1f 100644 --- a/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.ts +++ b/apps/gauzy/src/app/@shared/sidebar-menu/menu-items/concrete/menu-item/menu-item.component.ts @@ -9,12 +9,14 @@ import { Output, } from '@angular/core'; import { Router } from '@angular/router'; -import { NbSidebarService } from '@nebular/theme'; -import JitsuAnalyticsEvents from 'apps/gauzy/src/app/@core/services/analytics/event.type'; -import { JitsuService } from 'apps/gauzy/src/app/@core/services/analytics/jitsu.service'; -import { Store } from 'apps/gauzy/src/app/@core/services/store.service'; -import { IUser } from 'packages/contracts/dist'; import { tap } from 'rxjs/operators'; +import { NbSidebarService } from '@nebular/theme'; +import { IUser } from '@gauzy/contracts'; +import JitsuAnalyticsEvents, { + JitsuAnalyticsEventsEnum, +} from './../../../../../@core/services/analytics/event.type'; +import { JitsuService } from './../../../../../@core/services/analytics/jitsu.service'; +import { Store } from './../../../../../@core/services/store.service'; import { IMenuItem } from '../../interface/menu-item.interface'; @Component({ @@ -30,17 +32,15 @@ export class MenuItemComponent implements OnInit, AfterViewChecked { private _selected: boolean; private _user: IUser; - @Output() - public collapsedChange: EventEmitter = new EventEmitter(); - @Output() - public selectedChange: EventEmitter = new EventEmitter(); + @Output() public collapsedChange: EventEmitter = new EventEmitter(); + @Output() public selectedChange: EventEmitter = new EventEmitter(); constructor( - private router: Router, - private sidebarService: NbSidebarService, - private cdr: ChangeDetectorRef, - private location: Location, - private jitsuService: JitsuService, + private readonly router: Router, + private readonly sidebarService: NbSidebarService, + private readonly cdr: ChangeDetectorRef, + private readonly location: Location, + private readonly jitsuService: JitsuService, private readonly store: Store ) {} @@ -60,28 +60,40 @@ export class MenuItemComponent implements OnInit, AfterViewChecked { this.cdr.detectChanges(); } - public jitsuTrackClick() { + /** + * Track a click event. + * @param item The item that was clicked. + * @param user The user who clicked the item. + */ + public async jitsuTrackClick() { const clickEvent: JitsuAnalyticsEvents = { - eventType: 'ButtonClicked', + eventType: JitsuAnalyticsEventsEnum.BUTTON_CLICKED, url: this.item.url ?? this.item.link, userId: this._user.id, userEmail: this._user.email, menuItemName: this.item.title, }; - this.jitsuService.trackEvents(clickEvent.eventType, clickEvent); - this.jitsuService.identify(this._user.id, { + + // Identify the user + await this.jitsuService.identify(this._user.id, { email: this._user.email, fullName: this._user.name, timeZone: this._user.timeZone, }); - this.jitsuService.group(this._user.id, { + + // Group the user + await this.jitsuService.group(this._user.id, { email: this._user.email, fullName: this._user.name, timeZone: this._user.timeZone, }); + + // Track the click event + await this.jitsuService.trackEvents(clickEvent.eventType, clickEvent); } public redirectTo() { + // We don't await here because we don't want to wait for the analytics to complete before redirecting this.jitsuTrackClick(); if (!this.item.children) this.router.navigateByUrl(this.item.link); if (this.item.home) this.router.navigateByUrl(this.item.url); diff --git a/apps/gauzy/src/environments/model.ts b/apps/gauzy/src/environments/model.ts index 8ef5b3d7f40..41e223c9045 100644 --- a/apps/gauzy/src/environments/model.ts +++ b/apps/gauzy/src/environments/model.ts @@ -70,7 +70,7 @@ export interface Environment { FILE_PROVIDER: string; - JITSU_BROWSER_HOST?: string; + JITSU_BROWSER_URL?: string; JITSU_BROWSER_WRITE_KEY?: string; /** Github Integration */ diff --git a/docker-compose.demo.yml b/docker-compose.demo.yml index 5e7bb87a0b0..394d6c30e19 100644 --- a/docker-compose.demo.yml +++ b/docker-compose.demo.yml @@ -218,6 +218,11 @@ services: SENTRY_DSN: ${SENTRY_DSN:-} SENTRY_HTTP_TRACING_ENABLED: ${SENTRY_HTTP_TRACING_ENABLED:-} SENTRY_POSTGRES_TRACKING_ENABLED: ${SENTRY_POSTGRES_TRACKING_ENABLED:-} + JITSU_SERVER_URL: ${JITSU_SERVER_URL:-} + JITSU_SERVER_WRITE_KEY: ${JITSU_SERVER_WRITE_KEY:-} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_WEBHOOK_URL: ${GAUZY_GITHUB_WEBHOOK_URL:-} + GAUZY_GITHUB_WEBHOOK_SECRET: ${GAUZY_GITHUB_WEBHOOK_SECRET:-} env_file: - .env.compose entrypoint: './entrypoint.compose.sh' @@ -269,6 +274,13 @@ services: DEFAULT_LATITUDE: ${DEFAULT_LATITUDE:-42.6459136} DEFAULT_LONGITUDE: ${DEFAULT_LONGITUDE:-23.3332736} DEFAULT_CURRENCY: ${DEFAULT_CURRENCY:-USD} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-} + GAUZY_GITHUB_REDIRECT_URL: ${GAUZY_GITHUB_REDIRECT_URL:-} + GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-} + GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-} + JITSU_BROWSER_URL: ${JITSU_BROWSER_URL:-} + JITSU_BROWSER_WRITE_KEY: ${JITSU_BROWSER_WRITE_KEY:-} DEMO: 'true' API_HOST: ${API_HOST:-api} API_PORT: ${API_PORT:-3000} diff --git a/docker-compose.yml b/docker-compose.yml index c209e1dd5a8..4648bff9d14 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -225,6 +225,12 @@ services: SENTRY_DSN: ${SENTRY_DSN:-} SENTRY_HTTP_TRACING_ENABLED: ${SENTRY_HTTP_TRACING_ENABLED:-} SENTRY_POSTGRES_TRACKING_ENABLED: ${SENTRY_POSTGRES_TRACKING_ENABLED:-} + JITSU_SERVER_URL: ${JITSU_SERVER_URL:-} + JITSU_SERVER_WRITE_KEY: ${JITSU_SERVER_WRITE_KEY:-} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_WEBHOOK_URL: ${GAUZY_GITHUB_WEBHOOK_URL:-} + GAUZY_GITHUB_WEBHOOK_SECRET: ${GAUZY_GITHUB_WEBHOOK_SECRET:-} + env_file: - .env.compose entrypoint: './entrypoint.compose.sh' @@ -277,6 +283,13 @@ services: DEFAULT_LATITUDE: ${DEFAULT_LATITUDE:-42.6459136} DEFAULT_LONGITUDE: ${DEFAULT_LONGITUDE:-23.3332736} DEFAULT_CURRENCY: ${DEFAULT_CURRENCY:-USD} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-} + GAUZY_GITHUB_REDIRECT_URL: ${GAUZY_GITHUB_REDIRECT_URL:-} + GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-} + GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-} + JITSU_BROWSER_URL: ${JITSU_BROWSER_URL:-} + JITSU_BROWSER_WRITE_KEY: ${JITSU_BROWSER_WRITE_KEY:-} DEMO: 'true' API_HOST: ${API_HOST:-api} API_PORT: ${API_PORT:-3000} @@ -296,6 +309,13 @@ services: DEFAULT_LATITUDE: ${DEFAULT_LATITUDE:-42.6459136} DEFAULT_LONGITUDE: ${DEFAULT_LONGITUDE:-23.3332736} DEFAULT_CURRENCY: ${DEFAULT_CURRENCY:-USD} + GAUZY_GITHUB_CLIENT_ID: ${GAUZY_GITHUB_CLIENT_ID:-} + GAUZY_GITHUB_APP_NAME: ${GAUZY_GITHUB_APP_NAME:-} + GAUZY_GITHUB_REDIRECT_URL: ${GAUZY_GITHUB_REDIRECT_URL:-} + GAUZY_GITHUB_POST_INSTALL_URL: ${GAUZY_GITHUB_POST_INSTALL_URL:-} + GAUZY_GITHUB_APP_ID: ${GAUZY_GITHUB_APP_ID:-} + JITSU_BROWSER_URL: ${JITSU_BROWSER_URL:-} + JITSU_BROWSER_WRITE_KEY: ${JITSU_BROWSER_WRITE_KEY:-} DEMO: 'true' API_HOST: ${API_HOST:-api} API_PORT: ${API_PORT:-3000} diff --git a/packages/common/src/interfaces/IJitsuConfig.ts b/packages/common/src/interfaces/IJitsuConfig.ts new file mode 100644 index 00000000000..ffbbeda0034 --- /dev/null +++ b/packages/common/src/interfaces/IJitsuConfig.ts @@ -0,0 +1,24 @@ +/** + * Represents a configuration object for Jitsu server settings. + */ +export interface IJitsuConfig { + /** + * API Host. Default value: same host as script origin + */ + readonly serverHost: string; + + /** + * The write key for authenticating with the Jitsu server. + */ + readonly serverWriteKey: string; + + /** + * Whether to enable debug mode. + */ + readonly debug: boolean; + + /** + * Whether to echo events. + */ + readonly echoEvents: boolean; +} diff --git a/packages/common/src/interfaces/index.ts b/packages/common/src/interfaces/index.ts index 758058db10e..aca3abd8cd3 100644 --- a/packages/common/src/interfaces/index.ts +++ b/packages/common/src/interfaces/index.ts @@ -22,3 +22,4 @@ export * from './IUnleashConfig'; export * from './IUpworkConfig'; export * from './IWasabiConfig'; export * from './IHubstaffConfig'; +export * from './IJitsuConfig'; diff --git a/packages/config/src/environments/environment.prod.ts b/packages/config/src/environments/environment.prod.ts index 87c44de2afe..09f95f2b487 100644 --- a/packages/config/src/environments/environment.prod.ts +++ b/packages/config/src/environments/environment.prod.ts @@ -54,6 +54,16 @@ export const environment: IEnvironment = { THROTTLE_TTL: parseInt(process.env.THROTTLE_TTL) || 60, THROTTLE_LIMIT: parseInt(process.env.THROTTLE_LIMIT) || 300, + /** + * Jitsu Server Configuration + */ + jitsu: { + serverHost: process.env.JITSU_SERVER_URL, + serverWriteKey: process.env.JITSU_SERVER_WRITE_KEY, + debug: process.env.JITSU_SERVER_DEBUG === 'true' ? true : false, + echoEvents: process.env.JITSU_SERVER_ECHO_EVENTS === 'true' ? true : false, + }, + fileSystem: { name: (process.env.FILE_PROVIDER as FileStorageProviderEnum) || diff --git a/packages/config/src/environments/environment.ts b/packages/config/src/environments/environment.ts index e56e0eff8c6..22bd4127ac1 100644 --- a/packages/config/src/environments/environment.ts +++ b/packages/config/src/environments/environment.ts @@ -67,6 +67,16 @@ export const environment: IEnvironment = { THROTTLE_TTL: parseInt(process.env.THROTTLE_TTL) || 60, THROTTLE_LIMIT: parseInt(process.env.THROTTLE_LIMIT) || 300, + /** + * Jitsu Server Configuration + */ + jitsu: { + serverHost: process.env.JITSU_SERVER_URL, + serverWriteKey: process.env.JITSU_SERVER_WRITE_KEY, + debug: process.env.JITSU_SERVER_DEBUG === 'true' ? true : false, + echoEvents: process.env.JITSU_SERVER_ECHO_EVENTS === 'true' ? true : false, + }, + fileSystem: { name: (process.env.FILE_PROVIDER as FileStorageProviderEnum) || @@ -100,7 +110,8 @@ export const environment: IEnvironment = { api_key: process.env.CLOUDINARY_API_KEY, api_secret: process.env.CLOUDINARY_API_SECRET, secure: process.env.CLOUDINARY_API_SECURE === 'false' ? false : true, - delivery_url: process.env.CLOUDINARY_CDN_URL || `https://res.cloudinary.com`, + delivery_url: + process.env.CLOUDINARY_CDN_URL || `https://res.cloudinary.com`, }, facebookConfig: { @@ -109,21 +120,27 @@ export const environment: IEnvironment = { clientId: process.env.FACEBOOK_CLIENT_ID, clientSecret: process.env.FACEBOOK_CLIENT_SECRET, fbGraphVersion: process.env.FACEBOOK_GRAPH_VERSION, - oauthRedirectUri: process.env.FACEBOOK_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/facebook/callback`, + oauthRedirectUri: + process.env.FACEBOOK_CALLBACK_URL || + `${process.env.API_BASE_URL}/api/auth/facebook/callback`, state: '{fbstate}', }, googleConfig: { clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackUrl: process.env.GOOGLE_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/google/callback`, + callbackUrl: + process.env.GOOGLE_CALLBACK_URL || + `${process.env.API_BASE_URL}/api/auth/google/callback`, }, github: { /**Github OAuth Configuration */ clientId: process.env.GAUZY_GITHUB_CLIENT_ID, clientSecret: process.env.GAUZY_GITHUB_CLIENT_SECRET, - callbackUrl: process.env.GAUZY_GITHUB_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/github/callback`, + callbackUrl: + process.env.GAUZY_GITHUB_CALLBACK_URL || + `${process.env.API_BASE_URL}/api/auth/github/callback`, /** Github App Install Configuration */ appId: process.env.GAUZY_GITHUB_APP_ID, @@ -131,11 +148,15 @@ export const environment: IEnvironment = { appPrivateKey: process.env.GAUZY_GITHUB_APP_PRIVATE_KEY, /** Github App Post Install Configuration */ - postInstallUrl: process.env.GAUZY_GITHUB_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/github/setup/installation`, + postInstallUrl: + process.env.GAUZY_GITHUB_POST_INSTALL_URL || + `${process.env.CLIENT_BASE_URL}/#/pages/integrations/github/setup/installation`, /** Github Webhook Configuration */ webhookSecret: process.env.GAUZY_GITHUB_WEBHOOK_SECRET, - webhookUrl: process.env.GAUZY_GITHUB_WEBHOOK_URL || `${process.env.API_BASE_URL}/api/integration/github/webhook` + webhookUrl: + process.env.GAUZY_GITHUB_WEBHOOK_URL || + `${process.env.API_BASE_URL}/api/integration/github/webhook`, }, microsoftConfig: { @@ -143,19 +164,25 @@ export const environment: IEnvironment = { clientSecret: process.env.MICROSOFT_CLIENT_SECRET, resource: process.env.MICROSOFT_RESOURCE, tenant: process.env.MICROSOFT_TENANT, - callbackUrl: process.env.MICROSOFT_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/microsoft/callback`, + callbackUrl: + process.env.MICROSOFT_CALLBACK_URL || + `${process.env.API_BASE_URL}/api/auth/microsoft/callback`, }, linkedinConfig: { clientId: process.env.LINKEDIN_CLIENT_ID, clientSecret: process.env.LINKEDIN_CLIENT_SECRET, - callbackUrl: process.env.LINKEDIN_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/linked/callback`, + callbackUrl: + process.env.LINKEDIN_CALLBACK_URL || + `${process.env.API_BASE_URL}/api/auth/linked/callback`, }, twitterConfig: { clientId: process.env.TWITTER_CLIENT_ID, clientSecret: process.env.TWITTER_CLIENT_SECRET, - callbackUrl: process.env.TWITTER_CALLBACK_URL || `${process.env.API_BASE_URL}/api/auth/twitter/callback`, + callbackUrl: + process.env.TWITTER_CALLBACK_URL || + `${process.env.API_BASE_URL}/api/auth/twitter/callback`, }, fiverrConfig: { @@ -181,14 +208,18 @@ export const environment: IEnvironment = { dsn: process.env.SENTRY_DSN, }, - defaultIntegratedUserPass: process.env.INTEGRATED_USER_DEFAULT_PASS || '123456', - + defaultIntegratedUserPass: + process.env.INTEGRATED_USER_DEFAULT_PASS || '123456', upwork: { apiKey: process.env.UPWORK_API_KEY, apiSecret: process.env.UPWORK_API_SECRET, - callbackUrl: process.env.UPWORK_REDIRECT_URL || `${process.env.API_BASE_URL}/api/integrations/upwork/callback`, - postInstallUrl: process.env.UPWORK_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/upwork`, + callbackUrl: + process.env.UPWORK_REDIRECT_URL || + `${process.env.API_BASE_URL}/api/integrations/upwork/callback`, + postInstallUrl: + process.env.UPWORK_POST_INSTALL_URL || + `${process.env.CLIENT_BASE_URL}/#/pages/integrations/upwork`, }, hubstaff: { @@ -196,13 +227,15 @@ export const environment: IEnvironment = { clientId: process.env.HUBSTAFF_CLIENT_ID, clientSecret: process.env.HUBSTAFF_CLIENT_SECRET, /** Hubstaff Integration Post Install URL */ - postInstallUrl: process.env.HUBSTAFF_POST_INSTALL_URL || `${process.env.CLIENT_BASE_URL}/#/pages/integrations/hubstaff`, - + postInstallUrl: + process.env.HUBSTAFF_POST_INSTALL_URL || + `${process.env.CLIENT_BASE_URL}/#/pages/integrations/hubstaff`, }, isElectron: process.env.IS_ELECTRON === 'true' ? true : false, gauzyUserPath: process.env.GAUZY_USER_PATH, - allowSuperAdminRole: process.env.ALLOW_SUPER_ADMIN_ROLE === 'false' ? false : true, + allowSuperAdminRole: + process.env.ALLOW_SUPER_ADMIN_ROLE === 'false' ? false : true, /** * Endpoint for Gauzy AI API (optional), e.g.: http://localhost:3005/graphql diff --git a/packages/config/src/environments/ienvironment.ts b/packages/config/src/environments/ienvironment.ts index 6eaf1816495..ffa0b17c850 100644 --- a/packages/config/src/environments/ienvironment.ts +++ b/packages/config/src/environments/ienvironment.ts @@ -13,6 +13,7 @@ import { IGithubConfig, IGoogleConfig, IHubstaffConfig, + IJitsuConfig, IKeycloakConfig, ILinkedinConfig, IMicrosoftConfig, @@ -158,8 +159,7 @@ export interface IEnvironment { EMAIL_RESET_EXPIRATION_TIME?: number; /** - * Jitsu Config + * Jitsu Configuration */ - JITSU_BROWSER_HOST?: string; - JITSU_CONFIG_WRITE_KEY?: string; + jitsu: IJitsuConfig; } diff --git a/packages/contracts/src/organization-team-employee-model.ts b/packages/contracts/src/organization-team-employee-model.ts index 0bffca89ccd..55c53f0be58 100644 --- a/packages/contracts/src/organization-team-employee-model.ts +++ b/packages/contracts/src/organization-team-employee-model.ts @@ -7,10 +7,10 @@ import { ITask } from './task.model'; export interface IOrganizationTeamEmployee extends IBasePerTenantAndOrganizationEntityModel, - IRelationalOrganizationTeam, - IRelationalEmployee, - IRelationalRole, - ITimerStatus { + IRelationalOrganizationTeam, + IRelationalEmployee, + IRelationalRole, + ITimerStatus { isTrackingEnabled?: boolean; activeTaskId?: ITask['id']; activeTask?: ITask; @@ -18,10 +18,16 @@ export interface IOrganizationTeamEmployee export interface IOrganizationTeamEmployeeFindInput extends IBasePerTenantAndOrganizationEntityModel, - IRelationalOrganizationTeam {} + IRelationalOrganizationTeam { } export interface IOrganizationTeamEmployeeUpdateInput extends IBasePerTenantAndOrganizationEntityModel, - IRelationalOrganizationTeam { + IRelationalOrganizationTeam { isTrackingEnabled?: boolean; } + +export interface IOrganizationTeamEmployeeActiveTaskUpdateInput + extends IBasePerTenantAndOrganizationEntityModel, + IRelationalOrganizationTeam { + activeTaskId?: ITask['id']; +} diff --git a/packages/core/package.json b/packages/core/package.json index e30e7c85a9f..e9d8fd13b2f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -156,7 +156,8 @@ "upwork-api": "^1.3.8", "uuid": "^8.3.0", "web-push": "^3.4.4", - "yargs": "^17.5.0" + "yargs": "^17.5.0", + "@jitsu/js": "^1.3.0" }, "devDependencies": { "@nestjs/cli": "^9.1.5", diff --git a/packages/core/src/app.module.ts b/packages/core/src/app.module.ts index 41fefc21c79..be09a1e612c 100644 --- a/packages/core/src/app.module.ts +++ b/packages/core/src/app.module.ts @@ -153,7 +153,8 @@ import { EmailResetModule } from './email-reset/email-reset.module'; import { TaskLinkedIssueModule } from './tasks/linked-issue/task-linked-issue.module'; import { OrganizationTaskSettingModule } from './organization-task-setting/organization-task-setting.module'; import { TaskEstimationModule } from './tasks/estimation/task-estimation.module'; -const { unleashConfig, github } = environment; +import { JitsuAnalyticsModule } from './jitsu-analytics/jitsu-analytics.module'; +const { unleashConfig, github, jitsu } = environment; if (unleashConfig.url) { const unleashInstanceConfig: UnleashConfig = { @@ -250,7 +251,6 @@ if (environment.sentry && environment.sentry.dsn) { }), ] : []), - // Probot Configuration ProbotModule.forRoot({ isGlobal: true, @@ -264,7 +264,15 @@ if (environment.sentry && environment.sentry.dsn) { webhookSecret: github.webhookSecret }, }), - + /** Jitsu Configuration */ + JitsuAnalyticsModule.forRoot({ + config: { + host: jitsu.serverHost, + writeKey: jitsu.serverWriteKey, + debug: jitsu.debug, + echoEvents: jitsu.echoEvents + } + }), ThrottlerModule.forRootAsync({ inject: [ConfigService], useFactory: (config: ConfigService): ThrottlerModuleOptions => @@ -397,7 +405,7 @@ if (environment.sentry && environment.sentry.dsn) { IssueTypeModule, TaskLinkedIssueModule, OrganizationTaskSettingModule, - TaskEstimationModule + TaskEstimationModule, ], controllers: [AppController], providers: [ diff --git a/packages/core/src/core/entities/internal.ts b/packages/core/src/core/entities/internal.ts index 635a0dbd816..c1124ef8c21 100644 --- a/packages/core/src/core/entities/internal.ts +++ b/packages/core/src/core/entities/internal.ts @@ -184,3 +184,4 @@ export * from './../../time-tracking/screenshot/screenshot.subscriber'; export * from './../../time-tracking/time-slot/time-slot.subscriber'; export * from './../../user/user.subscriber'; export * from './../../integration/integration.subscriber'; +export * from '././../../jitsu-analytics/jitsu-events-subscriber'; diff --git a/packages/core/src/core/entities/subscribers.ts b/packages/core/src/core/entities/subscribers.ts index 52c661848b1..f98a2e90377 100644 --- a/packages/core/src/core/entities/subscribers.ts +++ b/packages/core/src/core/entities/subscribers.ts @@ -8,9 +8,11 @@ import { FeatureSubscriber, ImageAssetSubscriber, ImportHistorySubscriber, + IntegrationSubscriber, InviteSubscriber, InvoiceSubscriber, IssueTypeSubscriber, + JitsuEventsSubscriber, OrganizationContactSubscriber, OrganizationDocumentSubscriber, OrganizationProjectSubscriber, @@ -24,16 +26,15 @@ import { ScreenshotSubscriber, TagSubscriber, TaskPrioritySubscriber, - TaskSizeSubscriber, TaskRelatedIssueTypesSubscriber, + TaskSizeSubscriber, TaskStatusSubscriber, - TaskVersionSubscriber, TaskSubscriber, + TaskVersionSubscriber, TenantSubscriber, TimeOffRequestSubscriber, TimeSlotSubscriber, UserSubscriber, - IntegrationSubscriber, } from './internal'; /** @@ -53,6 +54,7 @@ export const coreSubscribers = [ InviteSubscriber, InvoiceSubscriber, IssueTypeSubscriber, + JitsuEventsSubscriber, OrganizationContactSubscriber, OrganizationDocumentSubscriber, OrganizationProjectSubscriber, @@ -74,5 +76,5 @@ export const coreSubscribers = [ TenantSubscriber, TimeOffRequestSubscriber, TimeSlotSubscriber, - UserSubscriber + UserSubscriber, ]; diff --git a/packages/core/src/database/migrations/1696704276300-AddLanguage.ts b/packages/core/src/database/migrations/1696704276300-AddLanguage.ts new file mode 100644 index 00000000000..7c77f3b5575 --- /dev/null +++ b/packages/core/src/database/migrations/1696704276300-AddLanguage.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { LanguageUtils } from '../../language/language-utils'; +import * as chalk from "chalk"; + +export class AddLanguage1696704276300 implements MigrationInterface { + name = 'AddLanguage1696704276300'; + + /** + * Up Migration + * + * @param queryRunner + */ + public async up(queryRunner: QueryRunner): Promise { + console.log(chalk.yellow(`AddLanguage1696704276300 start running!`)); + await LanguageUtils.migrateLanguages(queryRunner); + } + + /** + * Down Migration + * + * @param queryRunner + */ + public async down(queryRunner: QueryRunner): Promise {} +} diff --git a/packages/core/src/jitsu-analytics/jitsu-analytics.module.ts b/packages/core/src/jitsu-analytics/jitsu-analytics.module.ts new file mode 100644 index 00000000000..ff3631484e0 --- /dev/null +++ b/packages/core/src/jitsu-analytics/jitsu-analytics.module.ts @@ -0,0 +1,32 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { JitsuAnalyticsService } from './jitsu-analytics.service'; +import { JITSU_MODULE_PROVIDER_CONFIG, JitsuModuleOptions } from './jitsu.types'; + +@Module({ + providers: [ + JitsuAnalyticsService + ], +}) +export class JitsuAnalyticsModule { + /** + * Create a dynamic module for configuring and initializing the Jitsu Analytics module. + * @param options The options for configuring the Jitsu Analytics module. + * @returns A dynamic module definition. + */ + static forRoot(options: JitsuModuleOptions): DynamicModule { + return { + global: options.isGlobal || true, + module: JitsuAnalyticsModule, + providers: [ + { + provide: JITSU_MODULE_PROVIDER_CONFIG, + useFactory: () => options.config, + }, + JitsuAnalyticsService + ], + exports: [ + JitsuAnalyticsService + ], + }; + } +} diff --git a/packages/core/src/jitsu-analytics/jitsu-analytics.service.ts b/packages/core/src/jitsu-analytics/jitsu-analytics.service.ts new file mode 100644 index 00000000000..2e95178c426 --- /dev/null +++ b/packages/core/src/jitsu-analytics/jitsu-analytics.service.ts @@ -0,0 +1,81 @@ +import { Inject, Injectable, Logger } from '@nestjs/common'; +import { AnalyticsInterface, JitsuOptions } from '@jitsu/js'; +import { JITSU_MODULE_PROVIDER_CONFIG } from './jitsu.types'; +import { createJitsu } from './jitsu-helper'; + +@Injectable() +export class JitsuAnalyticsService { + private readonly logger = new Logger(JitsuAnalyticsService.name); + private readonly jitsu: AnalyticsInterface; + + constructor( + @Inject(JITSU_MODULE_PROVIDER_CONFIG) + private readonly config: JitsuOptions + ) { + try { + // Check if the required host and writeKey configuration properties are present + if (this.config.host && this.config.writeKey) { + // Initialize the Jitsu Analytics instance + this.jitsu = createJitsu(this.config); + } else { + this.logger.error( + `Jitsu Analytics initialization failed: Missing host or writeKey.` + ); + } + } catch (error) { + this.logger.error( + `Jitsu Analytics initialization failed: ${error.message}` + ); + } + } + + /** + * Track an analytics event using Jitsu Analytics. + * @param event The name of the event to track. + * @param properties Additional event properties (optional). + * @returns A promise that resolves when the event is tracked. + */ + async trackEvent( + event: string, + properties?: Record | null + ): Promise { + // Check if this.jitsu is defined and both host and writeKey are defined + if (this.jitsu && this.config.host && this.config.writeKey) { + return await this.jitsu.track(event, properties); + } else { + return null; // or handle it differently based on your requirements + } + } + + /** + * Identify a user with optional user traits. + * @param id The user identifier, such as a user ID or an object representing user information. + * @param traits User traits or properties to associate with the user. + * @returns A Promise that resolves when the user is identified. + */ + async identify( + id: string | object, + traits?: Record | null + ): Promise { + // Check if this.jitsu is defined and both host and writeKey are defined + if (this.jitsu && this.config.host && this.config.writeKey) { + return await this.jitsu.identify(id, traits); + } + } + + /** + * Group users into a specific segment or organization. + * @param id The identifier for the group, such as a group ID or an object representing group information. + * @param traits Additional data or traits associated with the group. + * @returns A Promise that resolves when the users are grouped. + */ + async group( + id: string | object, + traits?: Record | null + ): Promise { + // Check if this.jitsu is defined and both host and writeKey are defined + if (this.jitsu && this.config.host && this.config.writeKey) { + return await this.jitsu.group(id, traits); + } + } +} diff --git a/packages/core/src/jitsu-analytics/jitsu-events-subscriber.ts b/packages/core/src/jitsu-analytics/jitsu-events-subscriber.ts new file mode 100644 index 00000000000..8c302d73335 --- /dev/null +++ b/packages/core/src/jitsu-analytics/jitsu-events-subscriber.ts @@ -0,0 +1,179 @@ +import { AnalyticsInterface } from '@jitsu/js'; +import { Logger } from '@nestjs/common'; +import * as chalk from 'chalk'; +import { + InsertEvent, + RemoveEvent, + EntitySubscriberInterface, + UpdateEvent, + EventSubscriber, +} from 'typeorm'; +import { environment } from '@gauzy/config'; +import { createJitsu } from './jitsu-helper'; + +// Extract configuration values from environment +const { jitsu } = environment; + +/* Global Entity Subscriber - Listens to all entity +inserts updates and removal then sends to Jitsu */ +@EventSubscriber() +export class JitsuEventsSubscriber implements EntitySubscriberInterface { + private readonly logger = new Logger(JitsuEventsSubscriber.name); + private readonly jitsuAnalytics: AnalyticsInterface; + + // We disable by default additional logging for each event to avoid cluttering the logs + private logEnabled = false; + + constructor() { + try { + // Check if the required host and writeKey configuration properties are present + if (jitsu.serverHost && jitsu.serverWriteKey) { + const jitsuConfig = { + host: jitsu.serverHost, + writeKey: jitsu.serverWriteKey, + debug: jitsu.debug, + echoEvents: jitsu.echoEvents, + }; + this.logger.log( + `JITSU Configuration`, + chalk.magenta(JSON.stringify(jitsuConfig)) + ); + // Create an instance of Jitsu Analytics with configuration + this.jitsuAnalytics = createJitsu(jitsuConfig); + } else { + console.error( + chalk.yellow( + `Jitsu Analytics initialization failed: Missing host or writeKey.` + ) + ); + } + } catch (error) { + console.error( + chalk.red( + `Jitsu Analytics initialization failed: ${error.message}` + ) + ); + } + } + + /** + * Called after entity insertion. + */ + async afterInsert(event: InsertEvent) { + if (this.logEnabled) + this.logger.log( + `AFTER ENTITY INSERTED: `, + JSON.stringify(event.entity) + ); + + // Track an event with Jitsu Analytics + await this.analyticsTrack('afterInsert', { + data: { ...event.entity }, + }); + } + + /** + * Called after entity update. + */ + async afterUpdate(event: UpdateEvent) { + if (this.logEnabled) + this.logger.log( + `AFTER ENTITY UPDATED: `, + JSON.stringify(event.entity) + ); + + // Track an event with Jitsu Analytics + await this.analyticsTrack('afterUpdate', { + data: { ...event.entity }, + }); + } + + /** + * Called after entity removal. + */ + async afterRemove(event: RemoveEvent) { + if (this.logEnabled) + this.logger.log( + `AFTER ENTITY REMOVED: `, + JSON.stringify(event.entity) + ); + + // Track an event with Jitsu Analytics + await this.analyticsTrack('afterRemove', { + data: { ...event.entity }, + }); + } + + /** + * Track an analytics event using Jitsu Analytics. + * @param event The name of the event to track. + * @param properties Additional event properties (optional). + * @returns A promise that resolves when the event is tracked. + */ + async analyticsTrack( + event: string, + properties?: Record | null + ): Promise { + // Check if this.jitsu is defined and both host and writeKey are defined + if (this.jitsuAnalytics) { + try { + if (this.logEnabled) + this.logger.log( + `Before Jitsu Tracking Entity Events: ${event}`, + chalk.magenta(JSON.stringify(properties)) + ); + + const tracked = await this.trackEvent(event, properties); + + if (this.logEnabled) + this.logger.log( + `After Jitsu Tracked Entity Events`, + chalk.blue(JSON.stringify(tracked)) + ); + } catch (error) { + this.logger.error( + `Error while Jitsu tracking event. Unable to track event: ${error.message}` + ); + } + } + } + + /** + * Track an event with optional properties. + * @param event The name of the event to track. + * @param properties Additional data or properties associated with the event. + * @returns A Promise that resolves when the event is tracked. + */ + async trackEvent( + event: string, + properties?: Record | null + ): Promise { + return await this.jitsuAnalytics.track(event, properties); + } + + /** + * Identify a user with optional user traits. + * @param id The user identifier, such as a user ID or an object representing user information. + * @param traits User traits or properties to associate with the user. + * @returns A Promise that resolves when the user is identified. + */ + async identify( + id: string | object, + traits?: Record | null + ): Promise { + return await this.jitsuAnalytics.identify(id, traits); + } + + /** + * Group users into a specific segment or organization. + * @param id The identifier for the group, such as a group ID or an object representing group information. + * @param traits Additional data or traits associated with the group. + * @returns A Promise that resolves when the users are grouped. + */ + async group( + id: string | object, + traits?: Record | null + ): Promise { + return await this.jitsuAnalytics.group(id, traits); + } +} diff --git a/packages/core/src/jitsu-analytics/jitsu-helper.ts b/packages/core/src/jitsu-analytics/jitsu-helper.ts new file mode 100644 index 00000000000..69657375b9e --- /dev/null +++ b/packages/core/src/jitsu-analytics/jitsu-helper.ts @@ -0,0 +1,37 @@ +import { environment } from "@gauzy/config"; +import { AnalyticsInterface, JitsuOptions, jitsuAnalytics } from "@jitsu/js"; +import fetch from 'node-fetch'; + +/** + * Parse the configuration for Jitsu Analytics. + * @param config The input configuration object. + * @returns A record containing Jitsu configuration properties. + */ +export const parseConfig = (config: JitsuOptions): Record => ({ + host: config.host || environment.jitsu.serverHost || '', // Use serverHost from environment or empty string as default + writeKey: config.writeKey || environment.jitsu.serverWriteKey || '', // Use serverWriteKey from environment or empty string as default + debug: config.debug || false, // Use debug from input config or false as default + echoEvents: config.echoEvents || false, // Use echoEvents from input config or false as default +}); + +/** + * Create a Jitsu Analytics instance. + * @param opts The JitsuOptions object for configuration. + * @returns An instance of Jitsu Analytics. + */ +export const createJitsu = (opts: JitsuOptions): AnalyticsInterface => { + // Parse the configuration options + const config = parseConfig(opts); + if (!config.host || !config.writeKey) { + // Handle the case where 'host' or 'writeKey' is missing + console.error('Jitsu Analytics initialization failed: Missing host or writeKey.'); + return; + } + + config.fetch = fetch; // Assign the 'fetch' function to 'fetch' + console.log(`JITSU Configuration`, config); + // Create and return a Jitsu Analytics instance with the parsed configuration properties + return jitsuAnalytics({ + ...config, // Spread the parsed configuration properties + }); +}; diff --git a/packages/core/src/jitsu-analytics/jitsu.types.ts b/packages/core/src/jitsu-analytics/jitsu.types.ts new file mode 100644 index 00000000000..c989af9e0ac --- /dev/null +++ b/packages/core/src/jitsu-analytics/jitsu.types.ts @@ -0,0 +1,13 @@ +import { JitsuOptions } from "@jitsu/js"; + +// Provider key for Jitsu configuration +export const JITSU_MODULE_PROVIDER_CONFIG = 'JITSU_MODULE_PROVIDER_CONFIG'; + +// Define options for the Jitsu module +export interface JitsuModuleOptions { + // Specifies if the Jitsu module should be global + isGlobal?: boolean; + + // Jitsu configuration options + config: JitsuOptions; +} diff --git a/packages/core/src/language/language-utils.ts b/packages/core/src/language/language-utils.ts new file mode 100644 index 00000000000..e93f7d33690 --- /dev/null +++ b/packages/core/src/language/language-utils.ts @@ -0,0 +1,75 @@ +import { QueryRunner } from 'typeorm'; +import { ILanguage, LanguagesEnum } from '@gauzy/contracts'; +import allLanguages from './all-languages'; +import { Language } from './language.entity'; +import { faker } from '@faker-js/faker'; +import { v4 as uuidV4 } from 'uuid'; + +export class LanguageUtils { + private static async addLanguages( + queryRunner: QueryRunner, + languages: ILanguage[] + ): Promise { + for await (const language of languages) { + const { name, code, is_system, description, color } = language; + const payload = [name, code, is_system, description, color]; + let insertOrUpdateQuery = ''; + if ( + ['sqlite', 'better-sqlite3'].includes( + queryRunner.connection.options.type + ) + ) { + payload.push(uuidV4()); + insertOrUpdateQuery = ` + INSERT INTO language (name, code, is_system, description, color, id) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (code) + DO UPDATE SET + name = EXCLUDED.name, + is_system = EXCLUDED.is_system, + description = EXCLUDED.description, + color = EXCLUDED.color; + `; + } else { + insertOrUpdateQuery = ` + INSERT INTO language (name, code, is_system, description, color) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (code) + DO UPDATE SET + name = EXCLUDED.name, + is_system = EXCLUDED.is_system, + description = EXCLUDED.description, + color = EXCLUDED.color; + `; + } + await queryRunner.connection.manager.query( + insertOrUpdateQuery, + payload + ); + } + } + + public static get registeredLanguages(): ILanguage[] { + const systemLanguages: string[] = Object.values(LanguagesEnum); + const languages: ILanguage[] = []; + for (const key in allLanguages) { + if (Object.prototype.hasOwnProperty.call(allLanguages, key)) { + const { name, nativeName } = allLanguages[key]; + const language = new Language(); + language.name = name; + language.code = key; + language.is_system = systemLanguages.indexOf(key) >= 0; + language.description = nativeName; + language.color = faker.internet.color(); + languages.push(language); + } + } + return languages; + } + + public static async migrateLanguages( + queryRunner: QueryRunner + ): Promise { + await this.addLanguages(queryRunner, this.registeredLanguages); + } +} diff --git a/packages/core/src/organization-team-employee/dto/index.ts b/packages/core/src/organization-team-employee/dto/index.ts index ff0437a8fc9..6420c9a1ee2 100644 --- a/packages/core/src/organization-team-employee/dto/index.ts +++ b/packages/core/src/organization-team-employee/dto/index.ts @@ -1,2 +1,3 @@ export * from './delete-team-member-query.dto'; export * from './update-team-member.dto'; +export * from './update-organization-team-active-task.dto'; diff --git a/packages/core/src/organization-team-employee/dto/update-organization-team-active-task.dto.ts b/packages/core/src/organization-team-employee/dto/update-organization-team-active-task.dto.ts new file mode 100644 index 00000000000..5ad456f95da --- /dev/null +++ b/packages/core/src/organization-team-employee/dto/update-organization-team-active-task.dto.ts @@ -0,0 +1,12 @@ +import { IOrganizationTeamEmployeeUpdateInput } from '@gauzy/contracts'; +import { IntersectionType, PickType } from '@nestjs/swagger'; +import { OrganizationTeamEmployee } from '../../core/entities/internal'; +import { TenantOrganizationBaseDTO } from '../../core/dto'; + +/** + * Update team member active task entity DTO + */ +export class UpdateOrganizationTeamActiveTaskDTO extends IntersectionType( + TenantOrganizationBaseDTO, + PickType(OrganizationTeamEmployee, ['activeTaskId', 'organizationTeamId']), +) implements IOrganizationTeamEmployeeUpdateInput { } diff --git a/packages/core/src/organization-team-employee/dto/update-team-member.dto.ts b/packages/core/src/organization-team-employee/dto/update-team-member.dto.ts index ebc4c768e85..937b635c7de 100644 --- a/packages/core/src/organization-team-employee/dto/update-team-member.dto.ts +++ b/packages/core/src/organization-team-employee/dto/update-team-member.dto.ts @@ -1,18 +1,12 @@ import { IOrganizationTeamEmployeeUpdateInput } from '@gauzy/contracts'; import { IntersectionType, PickType } from '@nestjs/swagger'; import { OrganizationTeamEmployee } from './../../core/entities/internal'; -import { TenantOrganizationBaseDTO } from './../../core/dto'; +import { UpdateOrganizationTeamActiveTaskDTO } from './update-organization-team-active-task.dto'; /** * Update team member entity DTO */ -export class UpdateTeamMemberDTO - extends IntersectionType( - TenantOrganizationBaseDTO, - PickType(OrganizationTeamEmployee, [ - 'isTrackingEnabled', - 'organizationTeamId', - 'activeTaskId', - ]) - ) - implements IOrganizationTeamEmployeeUpdateInput {} +export class UpdateTeamMemberDTO extends IntersectionType( + UpdateOrganizationTeamActiveTaskDTO, + PickType(OrganizationTeamEmployee, ['isTrackingEnabled', 'organizationTeamId']) +) implements IOrganizationTeamEmployeeUpdateInput { } diff --git a/packages/core/src/organization-team-employee/organization-team-employee.controller.ts b/packages/core/src/organization-team-employee/organization-team-employee.controller.ts index fcb879f51a5..54a564cb357 100644 --- a/packages/core/src/organization-team-employee/organization-team-employee.controller.ts +++ b/packages/core/src/organization-team-employee/organization-team-employee.controller.ts @@ -6,7 +6,7 @@ import { PermissionGuard, TenantPermissionGuard } from './../shared/guards'; import { Permissions } from './../shared/decorators'; import { UUIDValidationPipe } from './../shared/pipes'; import { OrganizationTeamEmployeeService } from './organization-team-employee.service'; -import { DeleteTeamMemberQueryDTO, UpdateTeamMemberDTO } from './dto'; +import { DeleteTeamMemberQueryDTO, UpdateOrganizationTeamActiveTaskDTO, UpdateTeamMemberDTO } from './dto'; import { OrganizationTeamEmployee } from './organization-team-employee.entity'; @ApiTags('OrganizationTeamEmployee') @@ -37,6 +37,24 @@ export class OrganizationTeamEmployeeController { return await this.organizationTeamEmployeeService.update(memberId, entity); } + + /** + * Update organization team member active task entity + * + * @param id + * @param entity + * @returns + */ + @HttpCode(HttpStatus.ACCEPTED) + @UsePipes(new ValidationPipe({ whitelist: true })) + @Put(':id/active-task') + async updateActiveTask( + @Param('id', UUIDValidationPipe) memberId: IOrganizationTeamEmployee['id'], + @Body() entity: UpdateOrganizationTeamActiveTaskDTO + ): Promise { + return await this.organizationTeamEmployeeService.updateActiveTask(memberId, entity); + } + /** * Delete team member by memberId * diff --git a/packages/core/src/organization-team-employee/organization-team-employee.entity.ts b/packages/core/src/organization-team-employee/organization-team-employee.entity.ts index 60cd6789729..5ae33ee5a46 100644 --- a/packages/core/src/organization-team-employee/organization-team-employee.entity.ts +++ b/packages/core/src/organization-team-employee/organization-team-employee.entity.ts @@ -17,10 +17,7 @@ import { } from '../core/entities/internal'; @Entity('organization_team_employee') -export class OrganizationTeamEmployee - extends TenantOrganizationBaseEntity - implements IOrganizationTeamEmployee -{ +export class OrganizationTeamEmployee extends TenantOrganizationBaseEntity implements IOrganizationTeamEmployee { /** * enabled / disabled time tracking feature for team member */ @@ -28,7 +25,7 @@ export class OrganizationTeamEmployee @IsOptional() @IsBoolean() @Column({ type: Boolean, nullable: true, default: true }) - isTrackingEnabled?: boolean; + public isTrackingEnabled?: boolean; /* |-------------------------------------------------------------------------- @@ -40,7 +37,8 @@ export class OrganizationTeamEmployee * member's active task */ @ApiProperty({ type: () => Task }) - @ManyToOne(() => Task, (task) => task.organizationTeamEmployees, { + @ManyToOne(() => Task, (it) => it.organizationTeamEmployees, { + /** Database cascade action on delete. */ onDelete: 'CASCADE', }) public activeTask?: ITask; @@ -51,19 +49,16 @@ export class OrganizationTeamEmployee @RelationId((it: OrganizationTeamEmployee) => it.activeTask) @Index() @Column({ type: String, nullable: true }) - activeTaskId?: string; + public activeTaskId?: ITask['id']; /** * OrganizationTeam */ @ApiProperty({ type: () => OrganizationTeam }) - @ManyToOne( - () => OrganizationTeam, - (organizationTeam) => organizationTeam.members, - { - onDelete: 'CASCADE', - } - ) + @ManyToOne(() => OrganizationTeam, (it) => it.members, { + /** Database cascade action on delete. */ + onDelete: 'CASCADE' + }) public organizationTeam!: IOrganizationTeam; @ApiProperty({ type: () => String }) @@ -79,6 +74,7 @@ export class OrganizationTeamEmployee */ @ApiProperty({ type: () => Employee }) @ManyToOne(() => Employee, (employee) => employee.teams, { + /** Database cascade action on delete. */ onDelete: 'CASCADE', }) public employee: IEmployee; @@ -92,14 +88,19 @@ export class OrganizationTeamEmployee /** * Role */ - @ApiProperty({ type: () => Role }) + @ApiPropertyOptional({ type: () => Role }) @ManyToOne(() => Role, { + /** Indicates if relation column value can be nullable or not. */ nullable: true, + + /** Database cascade action on delete. */ onDelete: 'CASCADE', }) public role?: IRole; - @ApiProperty({ type: () => String, readOnly: true }) + @ApiPropertyOptional({ type: () => String }) + @IsOptional() + @IsUUID() @RelationId((it: OrganizationTeamEmployee) => it.role) @Index() @Column({ nullable: true }) diff --git a/packages/core/src/organization-team-employee/organization-team-employee.service.ts b/packages/core/src/organization-team-employee/organization-team-employee.service.ts index 0ee9179cb84..3d8f4d16e39 100644 --- a/packages/core/src/organization-team-employee/organization-team-employee.service.ts +++ b/packages/core/src/organization-team-employee/organization-team-employee.service.ts @@ -5,6 +5,7 @@ import { IEmployee, IOrganizationTeam, IOrganizationTeamEmployee, + IOrganizationTeamEmployeeActiveTaskUpdateInput, IOrganizationTeamEmployeeFindInput, IOrganizationTeamEmployeeUpdateInput, PermissionsEnum, @@ -67,8 +68,8 @@ export class OrganizationTeamEmployeeService extends TenantAwareCrudService { + + try { + const { organizationId, organizationTeamId } = entity; + const tenantId = RequestContext.currentTenantId(); + + // Admin, Super Admin can update activeTaskId of any Employee + if ( + RequestContext.hasPermission( + PermissionsEnum.CHANGE_SELECTED_EMPLOYEE + ) + ) { + const member = await this.repository.findOneOrFail({ + where: { + id: memberId, + tenantId, + organizationId, + organizationTeamId, + }, + }); + + return await this.repository.update(member.id, { activeTaskId: entity.activeTaskId }); + } else { + const employeeId = RequestContext.currentEmployeeId(); + if (employeeId) { + let member: OrganizationTeamEmployee; + try { + /** If employee has manager of the team, he/she should be able to update activeTaskId for team */ + await this.findOneByWhereOptions({ + organizationId, + organizationTeamId, + role: { + name: RolesEnum.MANAGER, + }, + }); + member = await this.repository.findOneOrFail({ + where: { + id: memberId, + organizationId, + tenantId, + organizationTeamId, + }, + }); + } catch (error) { + /** If employee has member of the team, he/she should be able to remove own self from team */ + member = await this.repository.findOneOrFail({ + where: { + employeeId, + organizationId, + tenantId, + organizationTeamId, + }, + }); + } + return await super.update({ id: member.id, organizationId, organizationTeamId }, { activeTaskId: entity.activeTaskId }); + + } + throw new ForbiddenException(); + } + } catch (error) { + throw new ForbiddenException(); + } + + + } + /** * Delete team member by memberId * diff --git a/packages/core/src/role-permission/default-role-permissions.ts b/packages/core/src/role-permission/default-role-permissions.ts index 959989ede02..00d0f273094 100644 --- a/packages/core/src/role-permission/default-role-permissions.ts +++ b/packages/core/src/role-permission/default-role-permissions.ts @@ -85,6 +85,7 @@ export const DEFAULT_ROLE_PERMISSIONS = [ PermissionsEnum.ORG_PROJECT_DELETE, PermissionsEnum.ORG_CONTACT_EDIT, PermissionsEnum.ORG_CONTACT_VIEW, + /** Organization Team */ PermissionsEnum.ORG_TEAM_ADD, PermissionsEnum.ORG_TEAM_VIEW, PermissionsEnum.ORG_TEAM_EDIT, @@ -218,6 +219,7 @@ export const DEFAULT_ROLE_PERMISSIONS = [ PermissionsEnum.ORG_PROJECT_DELETE, PermissionsEnum.ORG_CONTACT_EDIT, PermissionsEnum.ORG_CONTACT_VIEW, + /** Organization Team */ PermissionsEnum.ORG_TEAM_ADD, PermissionsEnum.ORG_TEAM_VIEW, PermissionsEnum.ORG_TEAM_EDIT, @@ -329,6 +331,7 @@ export const DEFAULT_ROLE_PERMISSIONS = [ PermissionsEnum.ORG_CONTACT_VIEW, PermissionsEnum.ORG_PROJECT_ADD, PermissionsEnum.ORG_PROJECT_VIEW, + /** Organization Team */ PermissionsEnum.ORG_TEAM_ADD, PermissionsEnum.ORG_TEAM_VIEW, PermissionsEnum.ORG_TEAM_EDIT,