feat: Add GA integration, support for GA4 (#4626)
* GA integration settings * trackingId -> measurementId Hook up script * Public page GA tracking Correct layout of settings * Remove multiple codepaths for loading GA measurementID, add missing db index * Remove unneccessary changes, tsc * test
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { IntegrationService } from "@shared/types";
|
||||
import env from "@server/env";
|
||||
import { IntegrationAuthentication, SearchQuery } from "@server/models";
|
||||
import { buildDocument, buildIntegration } from "@server/test/factories";
|
||||
@@ -14,7 +15,7 @@ describe("#hooks.unfurl", () => {
|
||||
it("should return documents", async () => {
|
||||
const { user, document } = await seed();
|
||||
await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
service: IntegrationService.Slack,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: "",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import crypto from "crypto";
|
||||
import Router from "koa-router";
|
||||
import { escapeRegExp } from "lodash";
|
||||
import { IntegrationService } from "@shared/types";
|
||||
import env from "@server/env";
|
||||
import { AuthenticationError, InvalidRequestError } from "@server/errors";
|
||||
import Logger from "@server/logging/Logger";
|
||||
@@ -67,7 +68,7 @@ router.post("hooks.unfurl", async (ctx) => {
|
||||
}
|
||||
const auth = await IntegrationAuthentication.findOne({
|
||||
where: {
|
||||
service: "slack",
|
||||
service: IntegrationService.Slack,
|
||||
teamId: user.teamId,
|
||||
},
|
||||
});
|
||||
@@ -240,7 +241,7 @@ router.post("hooks.slack", async (ctx) => {
|
||||
if (!user) {
|
||||
const auth = await IntegrationAuthentication.findOne({
|
||||
where: {
|
||||
service: "slack",
|
||||
service: IntegrationService.Slack,
|
||||
teamId: team.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Router from "koa-router";
|
||||
import { has } from "lodash";
|
||||
import { WhereOptions } from "sequelize";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Event } from "@server/models";
|
||||
@@ -21,17 +22,27 @@ const router = new Router();
|
||||
|
||||
router.post("integrations.list", auth(), pagination(), async (ctx) => {
|
||||
let { direction } = ctx.request.body;
|
||||
const { sort = "updatedAt" } = ctx.request.body;
|
||||
const { user } = ctx.state;
|
||||
const { type, sort = "updatedAt" } = ctx.request.body;
|
||||
if (direction !== "ASC") {
|
||||
direction = "DESC";
|
||||
}
|
||||
assertSort(sort, Integration);
|
||||
|
||||
const { user } = ctx.state;
|
||||
let where: WhereOptions<Integration> = {
|
||||
teamId: user.teamId,
|
||||
};
|
||||
|
||||
if (type) {
|
||||
assertIn(type, Object.values(IntegrationType));
|
||||
where = {
|
||||
...where,
|
||||
type,
|
||||
};
|
||||
}
|
||||
|
||||
const integrations = await Integration.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
},
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
|
||||
@@ -5,8 +5,10 @@ import { Context, Next } from "koa";
|
||||
import { escape } from "lodash";
|
||||
import { Sequelize } from "sequelize";
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import documentLoader from "@server/commands/documentLoader";
|
||||
import env from "@server/env";
|
||||
import { Integration } from "@server/models";
|
||||
import presentEnv from "@server/presenters/env";
|
||||
import { getTeamFromContext } from "@server/utils/passport";
|
||||
import prefetchTags from "@server/utils/prefetchTags";
|
||||
@@ -54,7 +56,12 @@ const readIndexFile = async (ctx: Context): Promise<Buffer> => {
|
||||
export const renderApp = async (
|
||||
ctx: Context,
|
||||
next: Next,
|
||||
options: { title?: string; description?: string; canonical?: string } = {}
|
||||
options: {
|
||||
title?: string;
|
||||
description?: string;
|
||||
canonical?: string;
|
||||
analytics?: Integration | null;
|
||||
} = {}
|
||||
) => {
|
||||
const {
|
||||
title = "Outline",
|
||||
@@ -69,7 +76,7 @@ export const renderApp = async (
|
||||
const { shareId } = ctx.params;
|
||||
const page = await readIndexFile(ctx);
|
||||
const environment = `
|
||||
window.env = ${JSON.stringify(presentEnv(env))};
|
||||
window.env = ${JSON.stringify(presentEnv(env, options.analytics))};
|
||||
`;
|
||||
ctx.body = page
|
||||
.toString()
|
||||
@@ -86,7 +93,7 @@ export const renderShare = async (ctx: Context, next: Next) => {
|
||||
// Find the share record if publicly published so that the document title
|
||||
// can be be returned in the server-rendered HTML. This allows it to appear in
|
||||
// unfurls with more reliablity
|
||||
let share, document;
|
||||
let share, document, analytics;
|
||||
|
||||
try {
|
||||
const team = await getTeamFromContext(ctx);
|
||||
@@ -105,6 +112,13 @@ export const renderShare = async (ctx: Context, next: Next) => {
|
||||
}
|
||||
document = result.document;
|
||||
|
||||
analytics = await Integration.findOne({
|
||||
where: {
|
||||
teamId: document.teamId,
|
||||
type: IntegrationType.Analytics,
|
||||
},
|
||||
});
|
||||
|
||||
if (share && !ctx.userAgent.isBot) {
|
||||
await share.update({
|
||||
lastAccessedAt: new Date(),
|
||||
@@ -123,6 +137,7 @@ export const renderShare = async (ctx: Context, next: Next) => {
|
||||
return renderApp(ctx, next, {
|
||||
title: document?.title,
|
||||
description: document?.getSummary(),
|
||||
analytics,
|
||||
canonical: share
|
||||
? `${share.canonicalUrl}${documentSlug && document ? document.url : ""}`
|
||||
: undefined,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { Context } from "koa";
|
||||
import Router from "koa-router";
|
||||
import { Profile } from "passport";
|
||||
import { Strategy as SlackStrategy } from "passport-slack-oauth2";
|
||||
import { IntegrationService, IntegrationType } from "@shared/types";
|
||||
import accountProvisioner from "@server/commands/accountProvisioner";
|
||||
import env from "@server/env";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
@@ -173,15 +174,15 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
const endpoint = `${env.URL}/auth/slack.commands`;
|
||||
const data = await Slack.oauthAccess(String(code), endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
service: IntegrationService.Slack,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
scopes: data.scope.split(","),
|
||||
});
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "command",
|
||||
service: IntegrationService.Slack,
|
||||
type: IntegrationType.Command,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
@@ -239,7 +240,7 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
const endpoint = `${env.URL}/auth/slack.post`;
|
||||
const data = await Slack.oauthAccess(code as string, endpoint);
|
||||
const authentication = await IntegrationAuthentication.create({
|
||||
service: "slack",
|
||||
service: IntegrationService.Slack,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
token: data.access_token,
|
||||
@@ -247,8 +248,8 @@ if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) {
|
||||
});
|
||||
|
||||
await Integration.create({
|
||||
service: "slack",
|
||||
type: "post",
|
||||
service: IntegrationService.Slack,
|
||||
type: IntegrationType.Post,
|
||||
userId: user.id,
|
||||
teamId: user.teamId,
|
||||
authenticationId: authentication.id,
|
||||
|
||||
@@ -5,9 +5,12 @@ import Router from "koa-router";
|
||||
import send from "koa-send";
|
||||
import userAgent, { UserAgentContext } from "koa-useragent";
|
||||
import { languages } from "@shared/i18n";
|
||||
import { IntegrationType } from "@shared/types";
|
||||
import env from "@server/env";
|
||||
import { NotFoundError } from "@server/errors";
|
||||
import { Integration } from "@server/models";
|
||||
import { opensearchResponse } from "@server/utils/opensearch";
|
||||
import { getTeamFromContext } from "@server/utils/passport";
|
||||
import { robotsResponse } from "@server/utils/robots";
|
||||
import apexRedirect from "../middlewares/apexRedirect";
|
||||
import { renderApp, renderShare } from "./app";
|
||||
@@ -118,7 +121,21 @@ router.get("/s/:shareId/doc/:documentSlug", renderShare);
|
||||
router.get("/s/:shareId/*", renderShare);
|
||||
|
||||
// catch all for application
|
||||
router.get("*", renderApp);
|
||||
router.get("*", async (ctx, next) => {
|
||||
const team = await getTeamFromContext(ctx);
|
||||
const analytics = team
|
||||
? await Integration.findOne({
|
||||
where: {
|
||||
teamId: team.id,
|
||||
type: IntegrationType.Analytics,
|
||||
},
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return renderApp(ctx, next, {
|
||||
analytics,
|
||||
});
|
||||
});
|
||||
|
||||
// In order to report all possible performance metrics to Sentry this header
|
||||
// must be provided when serving the application, see:
|
||||
|
||||
Reference in New Issue
Block a user