mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Merge branch 'dev' into swiftyos/secrt-1646-review-chat-logic-and-route-models-in-chatpy-and-modelspy
This commit is contained in:
@@ -4,13 +4,13 @@ import mimetypes
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from pydantic import SecretStr
|
||||
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from backend.data.model import APIKeyCredentials, SchemaField
|
||||
from backend.util.file import store_media_file
|
||||
from backend.util.request import Requests
|
||||
from backend.util.type import MediaFileType
|
||||
|
||||
from ._auth import (
|
||||
@@ -114,10 +114,9 @@ class ReadDiscordMessagesBlock(Block):
|
||||
if message.attachments:
|
||||
attachment = message.attachments[0] # Process the first attachment
|
||||
if attachment.filename.endswith((".txt", ".py")):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(attachment.url) as response:
|
||||
file_content = response.text()
|
||||
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
|
||||
response = await Requests().get(attachment.url)
|
||||
file_content = response.text()
|
||||
self.output_data += f"\n\nFile from user: {attachment.filename}\nContent: {file_content}"
|
||||
|
||||
await client.close()
|
||||
|
||||
@@ -699,16 +698,15 @@ class SendDiscordFileBlock(Block):
|
||||
|
||||
elif file.startswith(("http://", "https://")):
|
||||
# URL - download the file
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(file) as response:
|
||||
file_bytes = await response.read()
|
||||
response = await Requests().get(file)
|
||||
file_bytes = response.content
|
||||
|
||||
# Try to get filename from URL if not provided
|
||||
if not filename:
|
||||
from urllib.parse import urlparse
|
||||
# Try to get filename from URL if not provided
|
||||
if not filename:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
path = urlparse(file).path
|
||||
detected_filename = Path(path).name or "download"
|
||||
path = urlparse(file).path
|
||||
detected_filename = Path(path).name or "download"
|
||||
else:
|
||||
# Local file path - read from stored media file
|
||||
# This would be a path from a previous block's output
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
@@ -10,6 +8,7 @@ import pydantic
|
||||
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema
|
||||
from backend.data.model import SchemaField
|
||||
from backend.util.request import Requests
|
||||
|
||||
|
||||
class RSSEntry(pydantic.BaseModel):
|
||||
@@ -103,35 +102,29 @@ class ReadRSSFeedBlock(Block):
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_feed(url: str) -> dict[str, Any]:
|
||||
async def parse_feed(url: str) -> dict[str, Any]:
|
||||
# Security fix: Add protection against memory exhaustion attacks
|
||||
MAX_FEED_SIZE = 10 * 1024 * 1024 # 10MB limit for RSS feeds
|
||||
|
||||
# Validate URL
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
if parsed_url.scheme not in ("http", "https"):
|
||||
raise ValueError(f"Invalid URL scheme: {parsed_url.scheme}")
|
||||
|
||||
# Download with size limit
|
||||
# Download feed content with size limit
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=30) as response:
|
||||
# Check content length if available
|
||||
content_length = response.headers.get("Content-Length")
|
||||
if content_length and int(content_length) > MAX_FEED_SIZE:
|
||||
raise ValueError(
|
||||
f"Feed too large: {content_length} bytes exceeds {MAX_FEED_SIZE} limit"
|
||||
)
|
||||
response = await Requests(raise_for_status=True).get(url)
|
||||
|
||||
# Read with size limit
|
||||
content = response.read(MAX_FEED_SIZE + 1)
|
||||
if len(content) > MAX_FEED_SIZE:
|
||||
raise ValueError(
|
||||
f"Feed too large: exceeds {MAX_FEED_SIZE} byte limit"
|
||||
)
|
||||
# Check content length if available
|
||||
content_length = response.headers.get("Content-Length")
|
||||
if content_length and int(content_length) > MAX_FEED_SIZE:
|
||||
raise ValueError(
|
||||
f"Feed too large: {content_length} bytes exceeds {MAX_FEED_SIZE} limit"
|
||||
)
|
||||
|
||||
# Parse with feedparser using the validated content
|
||||
# feedparser has built-in protection against XML attacks
|
||||
return feedparser.parse(content) # type: ignore
|
||||
# Get content with size limit
|
||||
content = response.content
|
||||
if len(content) > MAX_FEED_SIZE:
|
||||
raise ValueError(f"Feed too large: exceeds {MAX_FEED_SIZE} byte limit")
|
||||
|
||||
# Parse with feedparser using the validated content
|
||||
# feedparser has built-in protection against XML attacks
|
||||
return feedparser.parse(content) # type: ignore
|
||||
except Exception as e:
|
||||
# Log error and return empty feed
|
||||
logging.warning(f"Failed to parse RSS feed from {url}: {e}")
|
||||
@@ -145,7 +138,7 @@ class ReadRSSFeedBlock(Block):
|
||||
while keep_going:
|
||||
keep_going = input_data.run_continuously
|
||||
|
||||
feed = self.parse_feed(input_data.rss_url)
|
||||
feed = await self.parse_feed(input_data.rss_url)
|
||||
all_entries = []
|
||||
|
||||
for entry in feed["entries"]:
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from typing import Type
|
||||
|
||||
from backend.blocks.ai_music_generator import AIMusicGeneratorBlock
|
||||
from backend.blocks.ai_shortform_video_block import AIShortformVideoCreatorBlock
|
||||
from backend.blocks.ai_shortform_video_block import (
|
||||
AIAdMakerVideoCreatorBlock,
|
||||
AIScreenshotToVideoAdBlock,
|
||||
AIShortformVideoCreatorBlock,
|
||||
)
|
||||
from backend.blocks.apollo.organization import SearchOrganizationsBlock
|
||||
from backend.blocks.apollo.people import SearchPeopleBlock
|
||||
from backend.blocks.apollo.person import GetPersonDetailBlock
|
||||
@@ -323,7 +327,31 @@ BLOCK_COSTS: dict[Type[Block], list[BlockCost]] = {
|
||||
],
|
||||
AIShortformVideoCreatorBlock: [
|
||||
BlockCost(
|
||||
cost_amount=50,
|
||||
cost_amount=307,
|
||||
cost_filter={
|
||||
"credentials": {
|
||||
"id": revid_credentials.id,
|
||||
"provider": revid_credentials.provider,
|
||||
"type": revid_credentials.type,
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
AIAdMakerVideoCreatorBlock: [
|
||||
BlockCost(
|
||||
cost_amount=714,
|
||||
cost_filter={
|
||||
"credentials": {
|
||||
"id": revid_credentials.id,
|
||||
"provider": revid_credentials.provider,
|
||||
"type": revid_credentials.type,
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
AIScreenshotToVideoAdBlock: [
|
||||
BlockCost(
|
||||
cost_amount=612,
|
||||
cost_filter={
|
||||
"credentials": {
|
||||
"id": revid_credentials.id,
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import bleach
|
||||
from bleach.css_sanitizer import CSSSanitizer
|
||||
from jinja2 import BaseLoader
|
||||
from jinja2.exceptions import TemplateError
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from markupsafe import Markup
|
||||
|
||||
@@ -101,8 +102,11 @@ class TextFormatter:
|
||||
|
||||
def format_string(self, template_str: str, values=None, **kwargs) -> str:
|
||||
"""Regular template rendering with escaping"""
|
||||
template = self.env.from_string(template_str)
|
||||
return template.render(values or {}, **kwargs)
|
||||
try:
|
||||
template = self.env.from_string(template_str)
|
||||
return template.render(values or {}, **kwargs)
|
||||
except TemplateError as e:
|
||||
raise ValueError(e) from e
|
||||
|
||||
def format_email(
|
||||
self,
|
||||
|
||||
@@ -33,7 +33,7 @@ Sentry.init({
|
||||
|
||||
enableLogs: true,
|
||||
integrations: [
|
||||
Sentry.captureConsoleIntegration(),
|
||||
Sentry.captureConsoleIntegration({ levels: ["fatal", "error", "warn"] }),
|
||||
Sentry.extraErrorDataIntegration(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/atoms/Button/Button";
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { Card } from "@/components/atoms/Card/Card";
|
||||
import { WaitlistErrorContent } from "@/components/auth/WaitlistErrorContent";
|
||||
import { isWaitlistErrorFromParams } from "@/app/api/auth/utils";
|
||||
import { isWaitlistError } from "@/app/api/auth/utils";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard";
|
||||
import { environment } from "@/services/environment";
|
||||
|
||||
export default function AuthErrorPage() {
|
||||
@@ -38,12 +38,9 @@ export default function AuthErrorPage() {
|
||||
}
|
||||
|
||||
// Check if this is a waitlist/not allowed error
|
||||
const isWaitlistError = isWaitlistErrorFromParams(
|
||||
errorCode,
|
||||
errorDescription,
|
||||
);
|
||||
const isWaitlistErr = isWaitlistError(errorCode, errorDescription);
|
||||
|
||||
if (isWaitlistError) {
|
||||
if (isWaitlistErr) {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Card className="w-full max-w-md p-8">
|
||||
@@ -56,34 +53,25 @@ export default function AuthErrorPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Default error display for other types of errors
|
||||
// Use ErrorCard for consistent error display
|
||||
const errorMessage = errorDescription
|
||||
? `${errorDescription}. If this error persists, please contact support at contact@agpt.co`
|
||||
: "An authentication error occurred. Please contact support at contact@agpt.co";
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center">
|
||||
<Card className="w-full max-w-md p-8">
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<Text variant="h3">Authentication Error</Text>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
{errorType && (
|
||||
<Text variant="body">
|
||||
<strong>Error Type:</strong> {errorType}
|
||||
</Text>
|
||||
)}
|
||||
{errorCode && (
|
||||
<Text variant="body">
|
||||
<strong>Error Code:</strong> {errorCode}
|
||||
</Text>
|
||||
)}
|
||||
{errorDescription && (
|
||||
<Text variant="body">
|
||||
<strong>Description:</strong> {errorDescription}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="primary" onClick={() => router.push("/login")}>
|
||||
Back to Login
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<div className="flex h-screen items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ErrorCard
|
||||
responseError={{
|
||||
message: errorMessage,
|
||||
detail: errorCode
|
||||
? `Error code: ${errorCode}${errorType ? ` (${errorType})` : ""}`
|
||||
: undefined,
|
||||
}}
|
||||
context="authentication"
|
||||
onRetry={() => router.push("/login")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Shepherd from "shepherd.js";
|
||||
import "shepherd.js/dist/css/shepherd.css";
|
||||
import { sendGAEvent } from "@/services/analytics/google-analytics";
|
||||
import { Key, storage } from "@/services/storage/local-storage";
|
||||
import { analytics } from "@/services/analytics";
|
||||
|
||||
export const startTutorial = (
|
||||
emptyNodeList: (forceEmpty: boolean) => boolean,
|
||||
@@ -555,7 +555,7 @@ export const startTutorial = (
|
||||
"use client";
|
||||
console.debug("sendTutorialStep");
|
||||
|
||||
sendGAEvent("event", "tutorial_step_shown", { value: step.id });
|
||||
analytics.sendGAEvent("event", "tutorial_step_shown", { value: step.id });
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,16 @@ function isVideoUrl(url: string): boolean {
|
||||
if (url.includes("youtube.com/watch") || url.includes("youtu.be/")) {
|
||||
return true;
|
||||
}
|
||||
if (url.includes("vimeo.com/")) {
|
||||
return true;
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (
|
||||
parsed.hostname === "vimeo.com" ||
|
||||
parsed.hostname === "www.vimeo.com"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// If URL parsing fails, treat as not a Vimeo URL.
|
||||
}
|
||||
return videoExtensions.some((ext) => url.toLowerCase().includes(ext));
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecu
|
||||
import { LibraryAgentPreset } from "@/app/api/__generated__/models/libraryAgentPreset";
|
||||
import { useGetV1GetUserTimezone } from "@/app/api/__generated__/endpoints/auth/auth";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
|
||||
export type RunVariant =
|
||||
| "manual"
|
||||
@@ -78,6 +79,10 @@ export function useAgentRunModal(
|
||||
agent.graph_id,
|
||||
).queryKey,
|
||||
});
|
||||
analytics.sendDatafastEvent("run_agent", {
|
||||
name: agent.name,
|
||||
id: agent.graph_id,
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
@@ -105,6 +110,11 @@ export function useAgentRunModal(
|
||||
agent.graph_id,
|
||||
),
|
||||
});
|
||||
analytics.sendDatafastEvent("schedule_agent", {
|
||||
name: agent.name,
|
||||
id: agent.graph_id,
|
||||
cronExpression: cronExpression,
|
||||
});
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -37,6 +37,7 @@ import { useToastOnFail } from "@/components/molecules/Toast/use-toast";
|
||||
import { AgentRunStatus, agentRunStatusMap } from "./agent-run-status-chip";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { AgentRunOutputView } from "./agent-run-output-view";
|
||||
import { analytics } from "@/services/analytics";
|
||||
|
||||
export function AgentRunDetailsView({
|
||||
agent,
|
||||
@@ -131,7 +132,13 @@ export function AgentRunDetailsView({
|
||||
run.inputs!,
|
||||
run.credential_inputs!,
|
||||
)
|
||||
.then(({ id }) => onRun(id))
|
||||
.then(({ id }) => {
|
||||
analytics.sendDatafastEvent("run_agent", {
|
||||
name: graph.name,
|
||||
id: graph.id,
|
||||
});
|
||||
onRun(id);
|
||||
})
|
||||
.catch(toastOnFail("execute agent preset"));
|
||||
}
|
||||
|
||||
@@ -142,7 +149,13 @@ export function AgentRunDetailsView({
|
||||
run.inputs!,
|
||||
run.credential_inputs!,
|
||||
)
|
||||
.then(({ id }) => onRun(id))
|
||||
.then(({ id }) => {
|
||||
analytics.sendDatafastEvent("run_agent", {
|
||||
name: graph.name,
|
||||
id: graph.id,
|
||||
});
|
||||
onRun(id);
|
||||
})
|
||||
.catch(toastOnFail("execute agent"));
|
||||
}, [api, graph, run, onRun, toastOnFail]);
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
|
||||
import { AgentStatus, AgentStatusChip } from "./agent-status-chip";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
|
||||
export function AgentRunDraftView({
|
||||
graph,
|
||||
@@ -197,6 +198,12 @@ export function AgentRunDraftView({
|
||||
}
|
||||
// Mark run agent onboarding step as completed
|
||||
completeOnboardingStep("MARKETPLACE_RUN_AGENT");
|
||||
|
||||
analytics.sendDatafastEvent("run_agent", {
|
||||
name: graph.name,
|
||||
id: graph.id,
|
||||
});
|
||||
|
||||
if (runCount > 0) {
|
||||
completeOnboardingStep("RE_RUN_AGENT");
|
||||
}
|
||||
@@ -373,6 +380,12 @@ export function AgentRunDraftView({
|
||||
})
|
||||
.catch(toastOnFail("set up agent run schedule"));
|
||||
|
||||
analytics.sendDatafastEvent("schedule_agent", {
|
||||
name: graph.name,
|
||||
id: graph.id,
|
||||
cronExpression: cronExpression,
|
||||
});
|
||||
|
||||
if (schedule && onCreateSchedule) onCreateSchedule(schedule);
|
||||
},
|
||||
[api, graph, inputValues, inputCredentials, onCreateSchedule, toastOnFail],
|
||||
|
||||
@@ -6,10 +6,10 @@ import Link from "next/link";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAgentInfo } from "./useAgentInfo";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
|
||||
interface AgentInfoProps {
|
||||
user: User | null;
|
||||
agentId: string;
|
||||
name: string;
|
||||
creator: string;
|
||||
shortDescription: string;
|
||||
@@ -20,11 +20,12 @@ interface AgentInfoProps {
|
||||
lastUpdated: string;
|
||||
version: string;
|
||||
storeListingVersionId: string;
|
||||
libraryAgent: LibraryAgent | undefined;
|
||||
isAgentAddedToLibrary: boolean;
|
||||
}
|
||||
|
||||
export const AgentInfo = ({
|
||||
user,
|
||||
agentId,
|
||||
name,
|
||||
creator,
|
||||
shortDescription,
|
||||
@@ -35,7 +36,7 @@ export const AgentInfo = ({
|
||||
lastUpdated,
|
||||
version,
|
||||
storeListingVersionId,
|
||||
libraryAgent,
|
||||
isAgentAddedToLibrary,
|
||||
}: AgentInfoProps) => {
|
||||
const {
|
||||
handleDownload,
|
||||
@@ -82,11 +83,15 @@ export const AgentInfo = ({
|
||||
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
|
||||
)}
|
||||
data-testid={"agent-add-library-button"}
|
||||
onClick={handleLibraryAction}
|
||||
disabled={isAddingAgentToLibrary}
|
||||
onClick={() =>
|
||||
handleLibraryAction({
|
||||
isAddingAgentFirstTime: !isAgentAddedToLibrary,
|
||||
})
|
||||
}
|
||||
>
|
||||
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
|
||||
{libraryAgent ? "See runs" : "Add to library"}
|
||||
{isAgentAddedToLibrary ? "See runs" : "Add to library"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
@@ -96,7 +101,7 @@ export const AgentInfo = ({
|
||||
"transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40",
|
||||
)}
|
||||
data-testid={"agent-download-button"}
|
||||
onClick={handleDownload}
|
||||
onClick={() => handleDownload(agentId, name)}
|
||||
disabled={isDownloadingAgent}
|
||||
>
|
||||
<div className="justify-start text-center font-sans text-sm font-medium leading-snug text-zinc-800">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { usePostV2AddMarketplaceAgent } from "@/app/api/__generated__/endpoints/library/library";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { useRouter } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { useGetV2DownloadAgentFile } from "@/app/api/__generated__/endpoints/store/store";
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { analytics } from "@/services/analytics";
|
||||
import { LibraryAgent } from "@/app/api/__generated__/models/libraryAgent";
|
||||
|
||||
interface UseAgentInfoProps {
|
||||
storeListingVersionId: string;
|
||||
@@ -16,29 +17,9 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
const { completeStep } = useOnboarding();
|
||||
|
||||
const {
|
||||
mutate: addMarketplaceAgentToLibrary,
|
||||
mutateAsync: addMarketplaceAgentToLibrary,
|
||||
isPending: isAddingAgentToLibrary,
|
||||
} = usePostV2AddMarketplaceAgent({
|
||||
mutation: {
|
||||
onSuccess: ({ data }) => {
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
router.push(`/library/agents/${(data as LibraryAgent).id}`);
|
||||
toast({
|
||||
title: "Agent Added",
|
||||
description: "Redirecting to your library...",
|
||||
duration: 2000,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
Sentry.captureException(error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to add agent to library. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
} = usePostV2AddMarketplaceAgent();
|
||||
|
||||
const { refetch: downloadAgent, isFetching: isDownloadingAgent } =
|
||||
useGetV2DownloadAgentFile(storeListingVersionId, {
|
||||
@@ -50,13 +31,46 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const handleLibraryAction = async () => {
|
||||
addMarketplaceAgentToLibrary({
|
||||
data: { store_listing_version_id: storeListingVersionId },
|
||||
});
|
||||
const handleLibraryAction = async ({
|
||||
isAddingAgentFirstTime,
|
||||
}: {
|
||||
isAddingAgentFirstTime: boolean;
|
||||
}) => {
|
||||
try {
|
||||
const { data: response } = await addMarketplaceAgentToLibrary({
|
||||
data: { store_listing_version_id: storeListingVersionId },
|
||||
});
|
||||
|
||||
const data = response as LibraryAgent;
|
||||
|
||||
if (isAddingAgentFirstTime) {
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
|
||||
analytics.sendDatafastEvent("add_to_library", {
|
||||
name: data.name,
|
||||
id: data.id,
|
||||
});
|
||||
}
|
||||
|
||||
router.push(`/library/agents/${data.id}`);
|
||||
|
||||
toast({
|
||||
title: "Agent Added",
|
||||
description: "Redirecting to your library...",
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to add agent to library. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
const handleDownload = async (agentId: string, agentName: string) => {
|
||||
try {
|
||||
const { data: file } = await downloadAgent();
|
||||
|
||||
@@ -74,6 +88,11 @@ export const useAgentInfo = ({ storeListingVersionId }: UseAgentInfoProps) => {
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
analytics.sendDatafastEvent("download_agent", {
|
||||
name: agentName,
|
||||
id: agentId,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: "Download Complete",
|
||||
description: "Your agent has been successfully downloaded.",
|
||||
|
||||
@@ -82,6 +82,7 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
<div className="w-full md:w-auto md:shrink-0">
|
||||
<AgentInfo
|
||||
user={user}
|
||||
agentId={agent.active_version_id ?? "–"}
|
||||
name={agent.agent_name}
|
||||
creator={agent.creator}
|
||||
shortDescription={agent.sub_heading}
|
||||
@@ -92,7 +93,7 @@ export const MainAgentPage = ({ params }: MainAgentPageProps) => {
|
||||
lastUpdated={agent.last_updated.toISOString()}
|
||||
version={agent.versions[agent.versions.length - 1]}
|
||||
storeListingVersionId={agent.store_listing_version_id}
|
||||
libraryAgent={libraryAgent}
|
||||
isAgentAddedToLibrary={Boolean(libraryAgent)}
|
||||
/>
|
||||
</div>
|
||||
<AgentImages
|
||||
|
||||
@@ -59,6 +59,7 @@ export function useSignupPage() {
|
||||
resetCaptcha();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/provider", {
|
||||
method: "POST",
|
||||
@@ -71,7 +72,6 @@ export function useSignupPage() {
|
||||
setIsGoogleLoading(false);
|
||||
resetCaptcha();
|
||||
|
||||
// Check for waitlist error
|
||||
if (error === "not_allowed") {
|
||||
setShowNotAllowedModal(true);
|
||||
return;
|
||||
@@ -149,6 +149,7 @@ export function useSignupPage() {
|
||||
setShowNotAllowedModal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
toast({
|
||||
title: result?.error || "Signup failed",
|
||||
variant: "destructive",
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function POST(request: Request) {
|
||||
|
||||
if (error) {
|
||||
// Check for waitlist/allowlist error
|
||||
if (isWaitlistError(error)) {
|
||||
if (isWaitlistError(error?.code, error?.message)) {
|
||||
logWaitlistError("OAuth Provider", error.message);
|
||||
return NextResponse.json({ error: "not_allowed" }, { status: 403 });
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export async function POST(request: Request) {
|
||||
turnstileToken ?? "",
|
||||
"signup",
|
||||
);
|
||||
|
||||
if (!captchaOk) {
|
||||
return NextResponse.json(
|
||||
{ error: "CAPTCHA verification failed. Please try again." },
|
||||
@@ -48,8 +49,7 @@ export async function POST(request: Request) {
|
||||
const { data, error } = await supabase.auth.signUp(parsed.data);
|
||||
|
||||
if (error) {
|
||||
// Check for waitlist/allowlist error
|
||||
if (isWaitlistError(error)) {
|
||||
if (isWaitlistError(error?.code, error?.message)) {
|
||||
logWaitlistError("Signup", error.message);
|
||||
return NextResponse.json({ error: "not_allowed" }, { status: 403 });
|
||||
}
|
||||
|
||||
@@ -1,49 +1,45 @@
|
||||
/**
|
||||
* Checks if a Supabase auth error is related to the waitlist/allowlist
|
||||
* Checks if an error is related to the waitlist/allowlist
|
||||
*
|
||||
* Can be used with either:
|
||||
* - Error objects from Supabase auth operations: `isWaitlistError(error?.code, error?.message)`
|
||||
* - URL parameters from OAuth callbacks: `isWaitlistError(errorCode, errorDescription)`
|
||||
*
|
||||
* The PostgreSQL trigger raises P0001 with message format:
|
||||
* "The email address "email" is not allowed to register. Please contact support for assistance."
|
||||
*
|
||||
* @param error - The error object from Supabase auth operations
|
||||
* @returns true if this is a waitlist/allowlist error
|
||||
*/
|
||||
export function isWaitlistError(error: any): boolean {
|
||||
if (!error?.message) return false;
|
||||
|
||||
if (error?.code === "P0001") return true;
|
||||
|
||||
return (
|
||||
error.message.includes("P0001") || // PostgreSQL custom error code
|
||||
error.message.includes("not allowed to register") || // Trigger message
|
||||
error.message.toLowerCase().includes("allowed_users") // Table reference
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if OAuth callback URL parameters indicate a waitlist error
|
||||
*
|
||||
* This is for the auth-code-error page which receives errors via URL hash params
|
||||
* from Supabase OAuth redirects
|
||||
*
|
||||
* @param errorCode - The error_code parameter from the URL
|
||||
* @param errorDescription - The error_description parameter from the URL
|
||||
* @param code - Error code (e.g., "P0001", "unexpected_failure") or null
|
||||
* @param message - Error message/description or null
|
||||
* @returns true if this appears to be a waitlist/allowlist error
|
||||
*/
|
||||
export function isWaitlistErrorFromParams(
|
||||
errorCode?: string | null,
|
||||
errorDescription?: string | null,
|
||||
export function isWaitlistError(
|
||||
code?: string | null,
|
||||
message?: string | null,
|
||||
): boolean {
|
||||
if (!errorDescription) return false;
|
||||
// Check for explicit PostgreSQL trigger error code
|
||||
if (code === "P0001") return true;
|
||||
|
||||
if (errorCode === "P0001") return true;
|
||||
if (!message) return false;
|
||||
|
||||
const description = errorDescription.toLowerCase();
|
||||
const lowerMessage = message.toLowerCase();
|
||||
|
||||
// Check for the generic database error that occurs during waitlist check
|
||||
// This happens when Supabase wraps the PostgreSQL trigger error
|
||||
if (
|
||||
code === "unexpected_failure" &&
|
||||
message === "Database error saving new user"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for various waitlist-related patterns in the message
|
||||
return (
|
||||
description.includes("p0001") || // PostgreSQL error code might be in description
|
||||
description.includes("not allowed") ||
|
||||
description.includes("waitlist") ||
|
||||
description.includes("allowlist") ||
|
||||
description.includes("allowed_users")
|
||||
lowerMessage.includes("p0001") || // PostgreSQL error code in message
|
||||
lowerMessage.includes("not allowed") || // Common waitlist message
|
||||
lowerMessage.includes("waitlist") || // Explicit waitlist mention
|
||||
lowerMessage.includes("allowlist") || // Explicit allowlist mention
|
||||
lowerMessage.includes("allowed_users") || // Database table reference
|
||||
lowerMessage.includes("not allowed to register") // Full trigger message
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,12 @@ import "./globals.css";
|
||||
|
||||
import { Providers } from "@/app/providers";
|
||||
import TallyPopupSimple from "@/components/molecules/TallyPoup/TallyPopup";
|
||||
import { GoogleAnalytics } from "@/services/analytics/google-analytics";
|
||||
import { Toaster } from "@/components/molecules/Toast/toaster";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import Script from "next/script";
|
||||
import { environment } from "@/services/environment";
|
||||
import { headers } from "next/headers";
|
||||
import { SetupAnalytics } from "@/services/analytics";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "AutoGPT Platform",
|
||||
@@ -27,7 +25,6 @@ export default async function RootLayout({
|
||||
}>) {
|
||||
const headersList = await headers();
|
||||
const host = headersList.get("host") || "";
|
||||
const withAnalytics = environment.areAnalyticsEnabled(host);
|
||||
|
||||
return (
|
||||
<html
|
||||
@@ -36,17 +33,12 @@ export default async function RootLayout({
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<GoogleAnalytics
|
||||
gaId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN"} // This is the measurement Id for the Google Analytics dev project
|
||||
<SetupAnalytics
|
||||
host={host}
|
||||
ga={{
|
||||
gaId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID || "G-FH2XK2W4GN",
|
||||
}}
|
||||
/>
|
||||
{withAnalytics ? (
|
||||
<Script
|
||||
strategy="afterInteractive"
|
||||
data-website-id="dfid_g5wtBIiHUwSkWKcGz80lu"
|
||||
data-domain="agpt.co"
|
||||
src="https://datafa.st/js/script.js"
|
||||
/>
|
||||
) : null}
|
||||
</head>
|
||||
<body>
|
||||
<Providers
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { StarRatingIcons } from "@/components/__legacy__/ui/icons";
|
||||
import { Separator } from "@/components/__legacy__/ui/separator";
|
||||
import BackendAPI, { LibraryAgent } from "@/lib/autogpt-server-api";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
|
||||
import { useOnboarding } from "@/providers/onboarding/onboarding-provider";
|
||||
import { User } from "@supabase/supabase-js";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { FC, useCallback, useMemo, useState } from "react";
|
||||
|
||||
interface AgentInfoProps {
|
||||
user: User | null;
|
||||
name: string;
|
||||
creator: string;
|
||||
shortDescription: string;
|
||||
longDescription: string;
|
||||
rating: number;
|
||||
runs: number;
|
||||
categories: string[];
|
||||
lastUpdated: string;
|
||||
version: string;
|
||||
storeListingVersionId: string;
|
||||
libraryAgent: LibraryAgent | null;
|
||||
}
|
||||
|
||||
export const AgentInfo: FC<AgentInfoProps> = ({
|
||||
user,
|
||||
name,
|
||||
creator,
|
||||
shortDescription,
|
||||
longDescription,
|
||||
rating,
|
||||
runs,
|
||||
categories,
|
||||
lastUpdated,
|
||||
version,
|
||||
storeListingVersionId,
|
||||
libraryAgent,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const api = useMemo(() => new BackendAPI(), []);
|
||||
const { toast } = useToast();
|
||||
const { completeStep } = useOnboarding();
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [downloading, setDownloading] = useState(false);
|
||||
|
||||
const libraryAction = useCallback(async () => {
|
||||
setAdding(true);
|
||||
if (libraryAgent) {
|
||||
toast({
|
||||
description: "Redirecting to your library...",
|
||||
duration: 2000,
|
||||
});
|
||||
// Redirect to the library agent page
|
||||
router.push(`/library/agents/${libraryAgent.id}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newLibraryAgent = await api.addMarketplaceAgentToLibrary(
|
||||
storeListingVersionId,
|
||||
);
|
||||
completeStep("MARKETPLACE_ADD_AGENT");
|
||||
router.push(`/library/agents/${newLibraryAgent.id}`);
|
||||
toast({
|
||||
title: "Agent Added",
|
||||
description: "Redirecting to your library...",
|
||||
duration: 2000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to add agent to library:", error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to add agent to library. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}, [toast, api, storeListingVersionId, completeStep, router]);
|
||||
|
||||
const handleDownload = useCallback(async () => {
|
||||
const downloadAgent = async (): Promise<void> => {
|
||||
setDownloading(true);
|
||||
try {
|
||||
const file = await api.downloadStoreAgent(storeListingVersionId);
|
||||
|
||||
// Similar to Marketplace v1
|
||||
const jsonData = JSON.stringify(file, null, 2);
|
||||
// Create a Blob from the file content
|
||||
const blob = new Blob([jsonData], { type: "application/json" });
|
||||
|
||||
// Create a temporary URL for the Blob
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `agent_${storeListingVersionId}.json`; // Set the filename
|
||||
|
||||
// Append the anchor to the body, click it, and remove it
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Revoke the temporary URL
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast({
|
||||
title: "Download Complete",
|
||||
description: "Your agent has been successfully downloaded.",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error downloading agent:`, error);
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to download agent. Please try again.",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
await downloadAgent();
|
||||
setDownloading(false);
|
||||
}, [setDownloading, api, storeListingVersionId, toast]);
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[396px] px-4 sm:px-6 lg:w-[396px] lg:px-0">
|
||||
{/* Title */}
|
||||
<div
|
||||
data-testid="agent-title"
|
||||
className="mb-3 w-full font-poppins text-2xl font-medium leading-normal text-neutral-900 dark:text-neutral-100 sm:text-3xl lg:mb-4 lg:text-[35px] lg:leading-10"
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
|
||||
{/* Creator */}
|
||||
<div className="mb-3 flex w-full items-center gap-1.5 lg:mb-4">
|
||||
<div className="text-base font-normal text-neutral-800 dark:text-neutral-200 sm:text-lg lg:text-xl">
|
||||
by
|
||||
</div>
|
||||
<Link
|
||||
data-testid={"agent-creator"}
|
||||
href={`/marketplace/creator/${encodeURIComponent(creator)}`}
|
||||
className="text-base font-medium text-neutral-800 hover:underline dark:text-neutral-200 sm:text-lg lg:text-xl"
|
||||
>
|
||||
{creator}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Short Description */}
|
||||
<div className="mb-4 line-clamp-2 w-full text-base font-normal leading-normal text-neutral-600 dark:text-neutral-300 sm:text-lg lg:mb-6 lg:text-xl lg:leading-7">
|
||||
{shortDescription}
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="mb-4 flex w-full gap-3 lg:mb-[60px]">
|
||||
{user && (
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex min-w-24 items-center justify-center rounded-full bg-violet-600 px-4 py-3",
|
||||
"transition-colors duration-200 hover:bg-violet-500 disabled:bg-zinc-400",
|
||||
)}
|
||||
data-testid={"agent-add-library-button"}
|
||||
onClick={libraryAction}
|
||||
disabled={adding}
|
||||
>
|
||||
<span className="justify-start font-sans text-sm font-medium leading-snug text-primary-foreground">
|
||||
{libraryAgent ? "See runs" : "Add to library"}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={cn(
|
||||
"inline-flex min-w-24 items-center justify-center rounded-full bg-zinc-200 px-4 py-3",
|
||||
"transition-colors duration-200 hover:bg-zinc-200/70 disabled:bg-zinc-200/40",
|
||||
)}
|
||||
data-testid={"agent-download-button"}
|
||||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
>
|
||||
<div className="justify-start text-center font-sans text-sm font-medium leading-snug text-zinc-800">
|
||||
Download agent
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Rating and Runs */}
|
||||
<div className="mb-4 flex w-full items-center justify-between lg:mb-[44px]">
|
||||
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||
<span className="whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{rating.toFixed(1)}
|
||||
</span>
|
||||
<div className="flex gap-0.5">{StarRatingIcons(rating)}</div>
|
||||
</div>
|
||||
<div className="whitespace-nowrap text-base font-semibold text-neutral-800 dark:text-neutral-200 sm:text-lg">
|
||||
{runs.toLocaleString()} runs
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Separator */}
|
||||
<Separator className="mb-4 lg:mb-[44px]" />
|
||||
|
||||
{/* Description Section */}
|
||||
<div className="mb-4 w-full lg:mb-[36px]">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Description
|
||||
</div>
|
||||
<div
|
||||
data-testid={"agent-description"}
|
||||
className="whitespace-pre-line text-base font-normal leading-6 text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
{longDescription}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<div className="mb-4 flex w-full flex-col gap-1.5 sm:gap-2 lg:mb-[36px]">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Categories
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5 sm:gap-2">
|
||||
{categories.map((category, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="decoration-skip-ink-none whitespace-nowrap rounded-full border border-neutral-600 bg-white px-2 py-0.5 text-base font-normal leading-6 text-neutral-800 underline-offset-[from-font] dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-200 sm:px-[16px] sm:py-[10px]"
|
||||
>
|
||||
{category}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version History */}
|
||||
<div className="flex w-full flex-col gap-0.5 sm:gap-1">
|
||||
<div className="decoration-skip-ink-none mb-1.5 text-base font-medium leading-6 text-neutral-800 dark:text-neutral-200 sm:mb-2">
|
||||
Version history
|
||||
</div>
|
||||
<div className="decoration-skip-ink-none text-base font-normal leading-6 text-neutral-600 underline-offset-[from-font] dark:text-neutral-400">
|
||||
Last updated {lastUpdated}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-600 dark:text-neutral-400 sm:text-sm">
|
||||
Version {version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -43,7 +43,15 @@ export function WaitlistErrorContent({
|
||||
exact same email address you used when signing up.
|
||||
</Text>
|
||||
<Text variant="small" className="text-center text-muted-foreground">
|
||||
If you're not sure which email you used or need help,{" "}
|
||||
If you're not sure which email you used or need help, contact us
|
||||
at{" "}
|
||||
<a
|
||||
href="mailto:contact@agpt.co"
|
||||
className="underline hover:text-foreground"
|
||||
>
|
||||
contact@agpt.co
|
||||
</a>{" "}
|
||||
or{" "}
|
||||
<a
|
||||
href="https://discord.gg/autogpt"
|
||||
target="_blank"
|
||||
|
||||
@@ -40,12 +40,11 @@ export function buildRequestUrl(
|
||||
method: string,
|
||||
payload?: Record<string, any>,
|
||||
): string {
|
||||
let url = baseUrl + path;
|
||||
const url = baseUrl + path;
|
||||
const payloadAsQuery = ["GET", "DELETE"].includes(method);
|
||||
|
||||
if (payloadAsQuery && payload) {
|
||||
const queryParams = new URLSearchParams(payload);
|
||||
url += `?${queryParams.toString()}`;
|
||||
return buildUrlWithQuery(url, payload);
|
||||
}
|
||||
|
||||
return url;
|
||||
@@ -61,12 +60,23 @@ export function buildServerUrl(path: string): string {
|
||||
|
||||
export function buildUrlWithQuery(
|
||||
url: string,
|
||||
payload?: Record<string, any>,
|
||||
query?: Record<string, any>,
|
||||
): string {
|
||||
if (!payload) return url;
|
||||
if (!query) return url;
|
||||
|
||||
const queryParams = new URLSearchParams(payload);
|
||||
return `${url}?${queryParams.toString()}`;
|
||||
// Filter out undefined values to prevent them from being included as "undefined" strings
|
||||
const filteredQuery = Object.entries(query).reduce(
|
||||
(acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
);
|
||||
|
||||
const queryParams = new URLSearchParams(filteredQuery);
|
||||
return queryParams.size > 0 ? `${url}?${queryParams.toString()}` : url;
|
||||
}
|
||||
|
||||
export async function handleFetchError(response: Response): Promise<ApiError> {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/**
|
||||
* Modified copy of ga.tsx from @next/third-parties/google, with modified gtag.js source URL.
|
||||
* Original source file: https://github.com/vercel/next.js/blob/b304b45e3a6e3e79338568d76e28805e77c03ec9/packages/third-parties/src/google/ga.tsx
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import type { GAParams } from "@/types/google";
|
||||
import Script from "next/script";
|
||||
import { useEffect } from "react";
|
||||
|
||||
let currDataLayerName: string | undefined = undefined;
|
||||
|
||||
export function GoogleAnalytics(props: GAParams) {
|
||||
const { gaId, debugMode, dataLayerName = "dataLayer", nonce } = props;
|
||||
|
||||
if (currDataLayerName === undefined) {
|
||||
currDataLayerName = dataLayerName;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Feature usage signal (same as original implementation)
|
||||
performance.mark("mark_feature_usage", {
|
||||
detail: {
|
||||
feature: "custom-ga",
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
id="_custom-ga-init"
|
||||
// Using "afterInteractive" to avoid blocking the initial page rendering
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window['${dataLayerName}'] = window['${dataLayerName}'] || [];
|
||||
function gtag(){window['${dataLayerName}'].push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gaId}' ${debugMode ? ",{ 'debug_mode': true }" : ""});
|
||||
`,
|
||||
}}
|
||||
nonce={nonce}
|
||||
/>
|
||||
<Script
|
||||
id="_custom-ga"
|
||||
strategy="afterInteractive"
|
||||
src="/gtag.js"
|
||||
nonce={nonce}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function sendGAEvent(...args: any[]) {
|
||||
if (currDataLayerName === undefined) {
|
||||
console.warn(`Custom GA: GA has not been initialized`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataLayer = (window as any)[currDataLayerName];
|
||||
if (dataLayer) {
|
||||
dataLayer.push(...args);
|
||||
} else {
|
||||
console.warn(`Custom GA: dataLayer ${currDataLayerName} does not exist`);
|
||||
}
|
||||
}
|
||||
114
autogpt_platform/frontend/src/services/analytics/index.tsx
Normal file
114
autogpt_platform/frontend/src/services/analytics/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Modified copy of ga.tsx from @next/third-parties/google, with modified gtag.js source URL.
|
||||
* Original source file: https://github.com/vercel/next.js/blob/b304b45e3a6e3e79338568d76e28805e77c03ec9/packages/third-parties/src/google/ga.tsx
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import type { GAParams } from "@/types/google";
|
||||
import Script from "next/script";
|
||||
import { useEffect } from "react";
|
||||
import { environment } from "../environment";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
datafast: (name: string, metadata: Record<string, unknown>) => void;
|
||||
[key: string]: unknown[] | ((...args: unknown[]) => void) | unknown;
|
||||
}
|
||||
}
|
||||
|
||||
let currDataLayerName: string | undefined = undefined;
|
||||
|
||||
type SetupProps = {
|
||||
ga: GAParams;
|
||||
host: string;
|
||||
};
|
||||
|
||||
export function SetupAnalytics(props: SetupProps) {
|
||||
const { ga, host } = props;
|
||||
const { gaId, debugMode, dataLayerName = "dataLayer", nonce } = ga;
|
||||
const isProductionDomain = host.includes("platform.agpt.co");
|
||||
|
||||
// Datafa.st journey analytics only on production
|
||||
const dataFastEnabled = isProductionDomain;
|
||||
// We collect analytics too for open source developers running the platform locally
|
||||
const googleAnalyticsEnabled = environment.isLocal() || isProductionDomain;
|
||||
|
||||
if (currDataLayerName === undefined) {
|
||||
currDataLayerName = dataLayerName;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!googleAnalyticsEnabled) return;
|
||||
|
||||
// Google Analytics: feature usage signal (same as original implementation)
|
||||
performance.mark("mark_feature_usage", {
|
||||
detail: {
|
||||
feature: "custom-ga",
|
||||
},
|
||||
});
|
||||
}, [googleAnalyticsEnabled]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Google Analytics */}
|
||||
{googleAnalyticsEnabled ? (
|
||||
<>
|
||||
<Script
|
||||
id="_custom-ga-init"
|
||||
strategy="afterInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window['${dataLayerName}'] = window['${dataLayerName}'] || [];
|
||||
function gtag(){window['${dataLayerName}'].push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '${gaId}' ${debugMode ? ",{ 'debug_mode': true }" : ""});
|
||||
`,
|
||||
}}
|
||||
nonce={nonce}
|
||||
/>
|
||||
{/* Google Tag Manager */}
|
||||
<Script
|
||||
id="_custom-ga"
|
||||
strategy="afterInteractive"
|
||||
src="/gtag.js"
|
||||
nonce={nonce}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{/* Datafa.st */}
|
||||
{dataFastEnabled ? (
|
||||
<Script
|
||||
strategy="afterInteractive"
|
||||
data-website-id="dfid_g5wtBIiHUwSkWKcGz80lu"
|
||||
data-domain="agpt.co"
|
||||
src="https://datafa.st/js/script.js"
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const analytics = {
|
||||
sendGAEvent,
|
||||
sendDatafastEvent,
|
||||
};
|
||||
|
||||
function sendGAEvent(...args: unknown[]) {
|
||||
if (typeof window === "undefined") return;
|
||||
if (currDataLayerName === undefined) return;
|
||||
|
||||
const dataLayer = window[currDataLayerName];
|
||||
if (!dataLayer) return;
|
||||
|
||||
if (Array.isArray(dataLayer)) {
|
||||
dataLayer.push(...args);
|
||||
} else {
|
||||
console.warn(`Custom GA: dataLayer ${currDataLayerName} does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
function sendDatafastEvent(name: string, metadata: Record<string, unknown>) {
|
||||
if (typeof window === "undefined" || !window.datafast) return;
|
||||
window.datafast(name, metadata);
|
||||
}
|
||||
@@ -92,10 +92,6 @@ function areFeatureFlagsEnabled() {
|
||||
return process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "enabled";
|
||||
}
|
||||
|
||||
function areAnalyticsEnabled(host: string) {
|
||||
return host.includes("platform.agpt.co");
|
||||
}
|
||||
|
||||
export const environment = {
|
||||
// Generic
|
||||
getEnvironmentStr,
|
||||
@@ -115,6 +111,5 @@ export const environment = {
|
||||
isCloud,
|
||||
isLocal,
|
||||
isCAPTCHAEnabled,
|
||||
areAnalyticsEnabled,
|
||||
areFeatureFlagsEnabled,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user