Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Learned patterns #395

Merged
merged 24 commits into from
Apr 8, 2025
Merged
Show file tree
Hide file tree
Changes from 16 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
67 changes: 67 additions & 0 deletions .cursor/rules/gmail-api.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
description: Guidelines for working with Gmail API
globs:
alwaysApply: false
---
# Gmail API Usage

Guidelines for working with email provider APIs (Gmail, Outlook, etc.) to ensure maintainability and future provider support.
Currently we only support Gmail.

## Core Principles

1. **Never call provider APIs directly from routes or components**
2. **Always use wrapper functions from the utils folder**
3. **Keep provider-specific implementation details isolated**

## Directory Structure

```
apps/web/utils/
├── gmail/ # Gmail-specific implementations
│ ├── message.ts # Message operations (get, list, batch, etc.)
│ ├── thread.ts # Thread operations
│ ├── label.ts # Label operations
│ └── ...
├── outlook/ # Future Outlook implementation
└── ... # Other providers
```

## Usage Patterns

### ✅ DO: Use the abstraction layers

```typescript
// GOOD: Using provided utility functions
import { getMessages, getMessage } from "@/utils/gmail/message";

async function fetchEmails(gmail: gmail_v1.Gmail, query: string) {
// Use the wrapper function that handles implementation details
const messages = await getMessages(gmail, {
query,
maxResults: 10,
});

return messages;
}
```

### ❌ DON'T: Call provider APIs directly

```typescript
// BAD: Direct API calls
async function fetchEmails(gmail: gmail_v1.Gmail, query: string) {
// Direct API calls make future provider support difficult
const response = await gmail.users.messages.list({
userId: "me",
q: query,
maxResults: 10,
});

return response.data;
}
```

## Why This Matters

1. **Future Provider Support**: By isolating provider-specific implementations, we can add support for Outlook, ProtonMail, etc.
4 changes: 2 additions & 2 deletions .cursor/rules/llm-test.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ Tests for LLM-related functionality should follow these guidelines to ensure con
// Skip tests unless explicitly running AI tests
const isAiTest = process.env.RUN_AI_TESTS === "true";

describe.skipIf(!isAiTest)("yourFunction", () => {
describe.runIf(isAiTest)("yourFunction", () => {
beforeEach(() => {
vi.clearAllMocks();
});

test("test case description", async () => {
// Test implementation
});
});
}, 15_000);
```

## Helper Functions
Expand Down
77 changes: 43 additions & 34 deletions apps/web/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,22 @@ DIRECT_URL="postgresql://postgres:password@localhost:5432/inboxzero?schema=publi
NEXTAUTH_SECRET= # Generate a random secret here: https://generate-secret.vercel.app/32
NEXTAUTH_URL=http://localhost:3000

# Gmail
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_ENCRYPT_SECRET= # openssl rand -hex 32
GOOGLE_ENCRYPT_SALT= # openssl rand -hex 16

GOOGLE_PUBSUB_TOPIC_NAME="projects/abc/topics/xyz"
GOOGLE_PUBSUB_VERIFICATION_TOKEN= # Generate a random secret here: https://generate-secret.vercel.app/32

# LLM config
DEFAULT_LLM_PROVIDER=openai
OPENAI_API_KEY=

# Set at least one of the following:
# ANTHROPIC_API_KEY=
# OPENROUTER_API_KEY=
# OPENAI_API_KEY=
# GOOGLE_API_KEY=
# GROQ_API_KEY=
# BEDROCK_ACCESS_KEY=
Expand All @@ -20,6 +28,13 @@ OPENAI_API_KEY=
# OLLAMA_BASE_URL=http://localhost:11434/api
# NEXT_PUBLIC_OLLAMA_MODEL=phi3

# Economy LLM configuration (for large context windows where cost efficiency matters)
ECONOMY_LLM_PROVIDER=
ECONOMY_LLM_MODEL=

INTERNAL_API_KEY= # Generate a random secret here: https://generate-secret.vercel.app/32
API_KEY_SALT= # Generate a random secret here: https://generate-secret.vercel.app/32

#redis config
UPSTASH_REDIS_URL="http://localhost:8079"
UPSTASH_REDIS_TOKEN= # Generate a random secret here: https://generate-secret.vercel.app/32
Expand All @@ -28,30 +43,47 @@ QSTASH_TOKEN=
QSTASH_CURRENT_SIGNING_KEY=
QSTASH_NEXT_SIGNING_KEY=

# Optional:

NEXT_PUBLIC_APP_HOME_PATH=/automation # If you want the product to default to email client, set this to /mail
LOG_ZOD_ERRORS=true
CRON_SECRET=

# Tinybird
TINYBIRD_TOKEN=
TINYBIRD_BASE_URL=https://api.us-east.tinybird.co/
TINYBIRD_ENCRYPT_SECRET= # openssl rand -hex 32
TINYBIRD_ENCRYPT_SALT= # openssl rand -hex 16
# Set this to true if you haven't set `TINYBIRD_TOKEN`.
# Some of the app's featues will be disabled when this is set.
# Generate a random secret here: https://generate-secret.vercel.app/32
API_KEY_SALT=

LOOPS_API_SECRET=

GOOGLE_PUBSUB_TOPIC_NAME="projects/abc/topics/xyz"
GOOGLE_PUBSUB_VERIFICATION_TOKEN= # Generate a random secret here: https://generate-secret.vercel.app/32

# Sentry (error tracking)
SENTRY_AUTH_TOKEN=
SENTRY_ORGANIZATION=
SENTRY_PROJECT=
NEXT_PUBLIC_SENTRY_DSN=

# Axiom (logging)
NEXT_PUBLIC_AXIOM_DATASET=
NEXT_PUBLIC_AXIOM_TOKEN=

LOG_ZOD_ERRORS=true
# PostHog (analytics)
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HERO_AB=
NEXT_PUBLIC_POSTHOG_ONBOARDING_SURVEY_ID=
POSTHOG_API_SECRET=
POSTHOG_PROJECT_ID=

# Marketing emails
RESEND_API_KEY=
LOOPS_API_SECRET=

# Crisp support chat
NEXT_PUBLIC_CRISP_WEBSITE_ID=

# Sanity config for blog
NEXT_PUBLIC_SANITY_PROJECT_ID=
NEXT_PUBLIC_SANITY_DATASET="production"

# Payments
LEMON_SQUEEZY_SIGNING_SECRET=
LEMON_SQUEEZY_API_KEY=

Expand All @@ -75,26 +107,3 @@ NEXT_PUBLIC_LIFETIME_PAYMENT_LINK=#
NEXT_PUBLIC_LIFETIME_VARIANT_ID=123
NEXT_PUBLIC_LIFETIME_EXTRA_SEATS_PAYMENT_LINK=#
NEXT_PUBLIC_LIFETIME_EXTRA_SEATS_VARIANT_ID=123

NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HERO_AB=
NEXT_PUBLIC_POSTHOG_ONBOARDING_SURVEY_ID=
POSTHOG_API_SECRET=
POSTHOG_PROJECT_ID=

RESEND_API_KEY=
CRON_SECRET=

NEXT_PUBLIC_CRISP_WEBSITE_ID=

ADMINS=

NEXT_PUBLIC_SANITY_PROJECT_ID=
NEXT_PUBLIC_SANITY_DATASET="production"

# SANITY_STUDIO_PROJECT_ID=
# SANITY_STUDIO_DATASET=production
# SANITY_STUDIO_HOST=

# If you want the product to default to email client, set this to /mail
NEXT_PUBLIC_APP_HOME_PATH=/automation
2 changes: 1 addition & 1 deletion apps/web/__tests__/ai-categorize-senders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const testSenders = [
},
];

describe.skipIf(!isAiTest)("AI Sender Categorization", () => {
describe.runIf(isAiTest)("AI Sender Categorization", () => {
describe("Bulk Categorization", () => {
it("should categorize senders with snippets using AI", async () => {
const result = await aiCategorizeSenders({
Expand Down
2 changes: 1 addition & 1 deletion apps/web/__tests__/ai-choose-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const isAiTest = process.env.RUN_AI_TESTS === "true";

vi.mock("server-only", () => ({}));

describe.skipIf(!isAiTest)("getActionItemsWithAiArgs", () => {
describe.runIf(isAiTest)("getActionItemsWithAiArgs", () => {
test("should return actions unchanged when no AI args needed", async () => {
const actions = [getAction({})];
const rule = getRule("Test rule", actions);
Expand Down
2 changes: 1 addition & 1 deletion apps/web/__tests__/ai-choose-rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const isAiTest = process.env.RUN_AI_TESTS === "true";

vi.mock("server-only", () => ({}));

describe.skipIf(!isAiTest)("aiChooseRule", () => {
describe.runIf(isAiTest)("aiChooseRule", () => {
test("Should return no rule when no rules passed", async () => {
const result = await aiChooseRule({
rules: [],
Expand Down
2 changes: 1 addition & 1 deletion apps/web/__tests__/ai-create-group.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ vi.mock("@/utils/gmail/message", () => ({
queryBatchMessages: vi.fn(),
}));

describe.skipIf(!isAiTest)("aiGenerateGroupItems", () => {
describe.runIf(isAiTest)("aiGenerateGroupItems", () => {
it("should generate group items based on user prompt", async () => {
const user = {
email: "[email protected]",
Expand Down
Loading