Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions integrations/google-analytics/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin/
package-lock.json
node_modules/
19 changes: 19 additions & 0 deletions integrations/google-analytics/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"rangeStart": 0,
"rangeEnd": null,
"requirePragma": false,
"insertPragma": false,
"proseWrap": "preserve",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}
65 changes: 65 additions & 0 deletions integrations/google-analytics/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import js from '@eslint/js';
import eslintConfigPrettier from 'eslint-config-prettier';
import importPlugin from 'eslint-plugin-import';
import eslintPluginPrettier from 'eslint-plugin-prettier';
import unusedImports from 'eslint-plugin-unused-imports';
import typescriptEslint from 'typescript-eslint';

export default [
js.configs.recommended,
...typescriptEslint.configs.recommended,
eslintConfigPrettier,
{
files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
globals: {
console: 'readonly',
process: 'readonly',
Buffer: 'readonly',
__dirname: 'readonly',
__filename: 'readonly',
global: 'readonly',
},
},
plugins: {
import: importPlugin,
prettier: eslintPluginPrettier,
'unused-imports': unusedImports,
},
rules: {
'prettier/prettier': 'error',
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'error',
'unused-imports/no-unused-vars': [
'warn',
{
vars: 'all',
varsIgnorePattern: '^_',
args: 'after-used',
argsIgnorePattern: '^_',
},
],
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
'newlines-between': 'always',
},
],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'warn',
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
},
];
48 changes: 48 additions & 0 deletions integrations/google-analytics/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "@core/google-analytics",
"version": "0.1.0",
"description": "Google Analytics integration for CORE",
"main": "./bin/index.cjs",
"type": "module",
"files": [
"google-analytics",
"bin"
],
"bin": {
"google-analytics": "./bin/index.cjs"
},
"scripts": {
"build": "tsup",
"lint": "eslint --ext js,ts,tsx src/ --fix",
"prettier": "prettier --config .prettierrc --write .",
"register": "tsx scripts/register.ts"
},
"devDependencies": {
"@babel/preset-typescript": "^7.26.0",
"@types/node": "^18.0.20",
"eslint": "^9.24.0",
"eslint-config-prettier": "^10.1.2",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^2.0.0",
"prettier": "^3.4.2",
"rimraf": "^3.0.2",
"tslib": "^2.8.1",
"tsup": "^8.0.1",
"tsx": "4.20.6",
"typescript": "^4.7.2"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@redplanethq/sdk": "0.1.14",
"axios": "^1.7.9",
"commander": "^12.0.0",
"pg": "^8.11.3",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.4"
}
}
79 changes: 79 additions & 0 deletions integrations/google-analytics/scripts/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import pg from 'pg';

const { Client } = pg;

async function main() {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error('DATABASE_URL environment variable is required');
process.exit(1);
}

const client = new Client({ connectionString });

const spec = {
name: 'Google Analytics',
key: 'google-analytics',
description:
'Connect your workspace to Google Analytics 4. Query reports, real-time data, funnels, and property metadata directly from CORE.',
icon: 'google-analytics',
mcp: {
type: 'cli',
},
auth: {
OAuth2: {
token_url: 'https://oauth2.googleapis.com/token',
authorization_url: 'https://accounts.google.com/o/oauth2/v2/auth',
scopes: [
'https://www.googleapis.com/auth/analytics.readonly',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
],
scope_identifier: 'scope',
scope_separator: ' ',
token_params: {
access_type: 'offline',
prompt: 'consent',
},
authorization_params: {
access_type: 'offline',
prompt: 'consent',
},
},
},
};

try {
await client.connect();

await client.query(
`
INSERT INTO core."IntegrationDefinitionV2" ("id", "name", "slug", "description", "icon", "spec", "config", "version", "url", "updatedAt", "createdAt")
VALUES (gen_random_uuid(), 'Google Analytics', 'google-analytics', 'Connect your workspace to Google Analytics 4. Query reports, real-time data, funnels, and property metadata directly from CORE.', 'google-analytics', $1, $2, '0.1.0', $3, NOW(), NOW())
ON CONFLICT (name) DO UPDATE SET
"slug" = EXCLUDED."slug",
"description" = EXCLUDED."description",
"icon" = EXCLUDED."icon",
"spec" = EXCLUDED."spec",
"config" = EXCLUDED."config",
"version" = EXCLUDED."version",
"url" = EXCLUDED."url",
"updatedAt" = NOW()
RETURNING *;
`,
[
JSON.stringify(spec),
JSON.stringify({}),
'../../integrations/google-analytics/bin/index.cjs',
]
);

console.log('Google Analytics integration registered successfully in the database.');
} catch (error) {
console.error('Error registering Google Analytics integration:', error);
} finally {
await client.end();
}
}

main().catch(console.error);
49 changes: 49 additions & 0 deletions integrations/google-analytics/src/account-create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import axios from 'axios';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export async function integrationCreate(data: any) {
const { oauthResponse, oauthParams } = data;

let userEmail: string | null = null;
let userId: string | null = null;

try {
const userInfoResponse = await axios.get('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${oauthResponse.access_token}`,
},
});

userEmail = userInfoResponse.data.email;
userId = userInfoResponse.data.id;
} catch (error) {
console.error('Error fetching user info:', error);
}

const integrationConfiguration = {
access_token: oauthResponse.access_token,
refresh_token: oauthResponse.refresh_token,
client_id: oauthResponse.client_id,
client_secret: oauthResponse.client_secret,
token_type: oauthResponse.token_type,
expires_in: oauthResponse.expires_in,
expires_at: oauthResponse.expires_at,
scope: oauthResponse.scope,
userEmail,
userId,
redirect_uri: oauthParams?.redirect_uri || null,
};

const payload = {
settings: {},
accountId: integrationConfiguration.userEmail || integrationConfiguration.userId,
config: integrationConfiguration,
};

return [
{
type: 'account',
data: payload,
},
];
}
105 changes: 105 additions & 0 deletions integrations/google-analytics/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { fileURLToPath } from 'url';

import {
IntegrationCLI,
IntegrationEventPayload,
IntegrationEventType,
Spec,
} from '@redplanethq/sdk';

import { integrationCreate } from './account-create';
import { callTool, getTools } from './mcp';

export async function run(eventPayload: IntegrationEventPayload) {
switch (eventPayload.event) {
case IntegrationEventType.SETUP:
return await integrationCreate(eventPayload.eventBody);

case IntegrationEventType.GET_TOOLS: {
const tools = await getTools();
return tools;
}

case IntegrationEventType.CALL_TOOL: {
const integrationDefinition = eventPayload.integrationDefinition;

if (!integrationDefinition) {
return null;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const config = eventPayload.config as any;
const { name, arguments: args } = eventPayload.eventBody;

const result = await callTool(
name,
args,
integrationDefinition.config.clientId,
integrationDefinition.config.clientSecret,
config?.redirect_uri,
config
);

return result;
}

default:
return { message: `The event payload type is ${eventPayload.event}` };
}
}

class GoogleAnalyticsCLI extends IntegrationCLI {
constructor() {
super('google-analytics', '1.0.0');
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected async handleEvent(eventPayload: IntegrationEventPayload): Promise<any> {
return await run(eventPayload);
}

protected async getSpec(): Promise<Spec> {
return {
name: 'Google Analytics',
key: 'google-analytics',
description:
'Connect your workspace to Google Analytics 4. Query reports, real-time data, funnels, and property metadata directly from CORE.',
icon: 'google-analytics',
mcp: {
type: 'cli',
},
auth: {
OAuth2: {
token_url: 'https://oauth2.googleapis.com/token',
authorization_url: 'https://accounts.google.com/o/oauth2/v2/auth',
scopes: [
'https://www.googleapis.com/auth/analytics',
'https://www.googleapis.com/auth/analytics.edit',
'https://www.googleapis.com/auth/analytics.readonly',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/userinfo.email',
],
scope_identifier: 'scope',
scope_separator: ' ',
token_params: {
access_type: 'offline',
prompt: 'consent',
},
authorization_params: {
access_type: 'offline',
prompt: 'consent',
},
},
},
};
}
}

function main() {
const cli = new GoogleAnalyticsCLI();
cli.parse();
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
main();
}
Loading