Dev GuideAPI Reference
Dev GuideUser GuidesGitHubDev CommunityOptimizely AcademySubmit a ticketLog In
Dev Guide

Google Analytics 4 Opal tools guide

A comprehensive guide to help you build an OCP app that implements Google Analytics 4 Opal tools.

This guide helps you build an Optimizely Connect Platform (OCP) app that implements Optimizely Opal tools. Optimizely based this implementation on the Optimizely-developed and publicly available Google Analytics 4 Opal tool. Access the tool in the OCP App Directory or directly through Opal.

Optimizely simplified and modified the code, removing internal references, such as references to internal monitoring tools. View the full source code of the reference app on Github.

Plan

Requirements

  • Define app functionality before starting. The app aims to build tools enabling Opal to access Google Analytics 4 data and request reports based on natural language prompts from users
    For example, if you provide the following prompt, Opal constructs a tool call with the required parameters and fetches results.
    Show me top pages visited via clickthroughs from chatgpt.com from last month
  • Define clear evaluation criteria by using a set of prompts. This is helpful due to the open-ended nature of AI agentic platforms.
    Optimizely's evaluation prompts include the following, which validate tool performance with Opal:
    • Show me how many pageviews our homepage had last month
    • How many visits from the Black Friday Facebook campaign did we get last month
    • What is the engagement rate with our pricing page?

Design

With defined requirements, start designing the app. Critical decisions include the following:

  • Main tools implemented by the app
  • Supporting tools
  • The API to fulfil requests
  • App authorization to the API
  • Additional app-level settings

The app accesses Google Analytics Data through the Google Analytics Data API. Optimizely uses two main methods from this API:

  • runReport –Returns a customized report of your Google Analytics event data. This is the preferred method for simple report queries.
  • runRealtimeReport – Gives Opal access to data from the present moment to 30 minutes ago (60 minutes for Google Analytics 360 properties).

Additionally, Optimizely implements the following supporting tools to ensure Opal uses the main tools correctly:

  • get_property_id – Returns the configured GA4 property ID, preventing Opal from asking users.
  • lookup_dimension_metric_names – Translates user-friendly display names, such as Active Users, to API names such as activeUsers.
  • get_dimensions – Lists all standard dimensions available for the property.
  • get_metrics – Lists all standard metrics available for the property.
  • get_custom_dimensions_and_metrics – Lists custom dimensions and metrics configured for the property.

Use these supporting tools to ensure a positive user experience. Without them, users require technical knowledge of API names and property IDs, creating friction.

The Google Analytics Data API offers various invocation methods. For this app, the most convenient method is to use the official Google Analytics Data: Node.js Client.

Google Analytics Data API supports multiple authorization methods. Use a method suitable for service-to-service communication with non-expiring tokens, as the app works outside of user sessions. The API offers two such methods, service account auth and Application Default Credentials (ADC). The app implements both, letting users choose based on their preference and governance rules.

🚧

Important

Authorization to Google Analytics Data API is organization-based. All users within an Optimizely customer organization use the same credentials. This is suitable to access reports. However, apps requiring per-user authorization must use custom OAuth providers.

The app also lets users define a custom hostname filter. When configured at the OCP account level, all queries to Google Analytics include this filter. This is useful when an organization hosts multiple hostnames in the same Google Analytics property, but Opal queries work in the context of one hostname.

In summary, the key design decisions include:

Prepare to code

  • Register the app name in OCP, which reserves the unique ID of the app:
    $ ocp app register
    ✔ The app id to reserve opal_tool_reference_app
    ✔ The display name of the app Google Analytics Opal Tool Reference App
    ✔ Target product for the app Connect Platform - for developing an app for Optimizely's holistic integration solution, Optimizely Connect Platform (OCP).
    ✔ Keep this app private and not share it with other developers in your organization? No
    Registering app opal_tool_reference_app in all shards
    Registered app opal_tool_reference_app with name "Google Analytics Opal Tool Reference App" in us
  • Select Connect Platform when prompted for the Target product for the app.
  • Scaffold the app:
    $ ocp app init
    Active environment: staging
    Name of your app (e.g., My App): Google Analytics Opal Tool Reference App
    ID of your app (e.g., my_app): opal_tool_reference_app
    Version [1.0.0-dev.1]: 1.0.0-dev.1
    App Summary (brief): Google Analytics Opal Tool Reference App
    Support URL: https://support.optimizely.com
    Contact email address: [email protected]
    Select the category for the app: Opal
    Select a template project: Opal tool OCP app
    Creating directory /Users/john.smith/apps/opal-tool-reference-app
    Performing initial Yarn install
    yarn install v1.22.22
    warning ../../package.json: No license field
    info No lockfile found.
    [1/4] 🔍  Resolving packages...
    [2/4] 🚚  Fetching packages...
    [3/4] 🔗  Linking dependencies...
    [4/4] 🔨  Building fresh packages...
    success Saved lockfile.
    ✨  Done in 10.58s.
    
    New OCP app project created at /Users/john.smith/apps/opal-tool-reference-app
    
        * View README.md for information on getting started
        * Check out the documentation (https://docs.developers.optimizely.com/optimizely-connect-platform/docs)
  • Use Opal tool OCP app as the template project. This template provides a strong foundation to develop Opal tools.
  • Select Opal when prompted for the category. This ensures the app displays correctly in the App Directory.

Implement

Connect to Google Analytics Data API

This section details configuring authentication to the Google Analytics Data API, including creating a settings form for credential entry, implementing the API client, and validating credentials.

Install dependencies

Add google-auth-library and googleapis to package.json. The app uses these libraries to authenticate and call the Google Analytics Data API.

yarn add google-auth-library
yarn add googleapis

Define the settings form

📘

Note

The Google Analytics 4 Opal tools app implements per-organization authentication. The OCP admin configures and authenticates the app in OCP, and all users within that organization share the same credentials when accessing Google Analytics data through Opal.

Create forms/settings.yml to collect authentication credentials from users. The form supports two authentication methods, service account auth and Application Default Credentials (ADC)

sections:
  - key: authentication
    label: Authenticate Google Analytics
    elements:
      - type: select
        key: auth_method
        label: Authentication Method
        help: Choose how to authenticate with Google Analytics API
        required: true
        options:
          - text: Service Account Authentication
            value: service_account
          - text: Application Default Credentials (ADC)
            value: adc
      - type: divider
      - type: instructions
        visible:
          key: authentication.auth_method
          equals: service_account
        text: |
          **Service Account Authentication:**
          - Create a service account in Google Cloud Console
          - Grant the service account access to your GA4 properties
          - Download the JSON key and paste its contents in the field below
      - type: instructions
        visible:
          key: authentication.auth_method
          equals: adc
        text: |
          **Application Default Credentials (ADC):**
          - Recommended when service account keys are disabled by organization policy
          - Run `gcloud auth application-default login` with required scopes
          - Copy credentials from `application_default_credentials.json`
      - type: secret
        key: service_account_json
        label: Service Account JSON
        help: Paste the complete service account credentials JSON file content
        validations:
          - predicate:
              operation: any
              comparators:
              - key: service_account_json
                empty: false
              - key: auth_method
                equals: 'adc'
            message: The field is required for Service Account Authentication
        visible:
          key: authentication.auth_method
          equals: service_account
      - type: text
        key: client_id
        label: Client ID
        help: Your ADC client ID
        validations:
          - predicate:
              operation: any
              comparators:
              - key: client_id
                empty: false
              - key: auth_method
                equals: 'service_account'
            message: The field is required for ADC authentication
        visible:
          key: authentication.auth_method
          equals: adc
      - type: secret
        key: client_secret
        label: Client Secret
        help: Your ADC client secret
        visible:
          key: authentication.auth_method
          equals: adc
      - type: secret
        key: refresh_token
        label: Refresh Token
        help: Your ADC refresh token
        visible:
          key: authentication.auth_method
          equals: adc
      - type: text
        key: project_id
        label: Google Cloud Project ID
        help: The GCP project ID associated with your Google Analytics property
        required: true
        visible:
          key: authentication.auth_method
          empty: false
      - type: text
        key: property_id
        label: Google Analytics property ID
        required: true
        help: "Accepted formats: number or properties/{property_id}"
        visible:
          key: authentication.auth_method
          empty: false
      - type: button
        label: Authenticate
        style: primary
        action: authorize

The key form features include:

  • visible – Conditionally displays fields based on the selected authentication method. The key uses dot notation (authentication.auth_method) to reference the section and field.
  • type: secret – Masks sensitive fields like credentials and tokens in the UI.
  • type: divider – Adds visual separation between form sections.
  • validations with predicate – Implements conditional validation. The any operation requires at least one true comparator, making fields optional when another auth method is selected.
  • type: button – Triggers form submission with the action: authorize handler.

Define TypeScript interfaces

Create src/data/data.ts to define the authentication settings structure. This interface extends App.ValueHash to ensure compatibility with OCP's storage system.

import * as App from '@zaiusinc/app-sdk';

export interface AuthSection extends App.ValueHash {
  auth_method: 'service_account' | 'adc';
  service_account_json?: string;
  client_id?: string;
  client_secret?: string;
  refresh_token?: string;
  project_id?: string;
  property_id?: string;
}

Implement the Google Analytics API client

Create src/lib/api/GoogleAnalyticsApiClient.ts to handle authentication and create API clients by using the following steps:

  • Define the required OAuth scopes:
    import { GoogleAuth } from 'google-auth-library';
    import { analyticsdata_v1beta, google } from 'googleapis';
    import { AuthSection } from '../../data/data';
    
    /**
     * Required OAuth scopes for Google Analytics API access
     */
    export const REQUIRED_SCOPES = [
      'https://www.googleapis.com/auth/cloud-platform',
      'https://www.googleapis.com/auth/analytics.readonly',
    ];
  • Create a factory function to build the configuration from user settings:
    /**
     * Creates a client configuration from app settings
     */
    export function createConfigFromSettings(
      settings: AuthSection
    ): GoogleAnalyticsClientConfig {
      if (!settings) {
        throw new Error('App settings are required for authentication');
      }
    
      const authMethod = settings.auth_method;
      const projectId = settings.project_id;
    
      if (!authMethod) {
        throw new Error('Authentication method must be specified in app settings');
      }
    
      if (!projectId) {
        throw new Error('Google Cloud Project ID is required in app settings');
      }
    
      switch (authMethod) {
        case 'service_account':
          if (!settings.service_account_json) {
            throw new Error(
              'Service Account JSON is required when using service account authentication'
            );
          }
          const credentials = JSON.parse(settings.service_account_json);
          return { credentials, projectId };
    
        case 'adc':
          if (
            !settings.client_id ||
            !settings.client_secret ||
            !settings.refresh_token
          ) {
            throw new Error(
              'ADC credentials require client_id, client_secret, and refresh_token'
            );
          }
          return {
            credentials: {
              type: 'authorized_user' as const,
              client_id: settings.client_id,
              client_secret: settings.client_secret,
              refresh_token: settings.refresh_token,
              quota_project_id: projectId,
            },
            projectId,
          };
    
        default:
          throw new Error(`Unsupported authentication method: ${authMethod}`);
      }
    }
  • Create the AppAuth class that wraps the Google Auth library:
    export class AppAuth {
      private auth: GoogleAuth;
      private dataClient: analyticsdata_v1beta.Analyticsdata;
    
      public constructor(config: GoogleAnalyticsClientConfig = {}) {
        // Initialize authentication with required scopes
        this.auth = new GoogleAuth({
          keyFilename: config.keyFilename,
          credentials: config.credentials,
          scopes: REQUIRED_SCOPES,
          projectId: config.projectId,
        });
    
        this.dataClient = google.analyticsdata({
          version: 'v1beta',
          auth: this.auth,
        });
      }
    
      /**
       * Creates AppAuth instance from app settings
       */
      public static fromSettings(settings: AuthSection): AppAuth {
        const config = createConfigFromSettings(settings);
        return new AppAuth(config);
      }
    
      /**
       * Validates that the credentials have the required OAuth scopes
       */
      public async validateScopes(): Promise<void> {
        const client = await this.auth.getClient();
        const tokenInfo = await client.getAccessToken();
    
        if (!tokenInfo.token) {
          throw new Error('Failed to obtain access token');
        }
    
        // Verify token scopes via Google's OAuth2 API
        const response = await fetch(
          `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${tokenInfo.token}`
        );
    
        if (!response.ok) {
          throw new Error(`Failed to verify token: ${response.statusText}`);
        }
    
        const tokenData = (await response.json()) as { scope?: string };
        const grantedScopes = tokenData.scope ? tokenData.scope.split(' ') : [];
    
        const missingScopes = REQUIRED_SCOPES.filter(
          (requiredScope) => !grantedScopes.includes(requiredScope)
        );
    
        if (missingScopes.length > 0) {
          throw new Error(
            `Missing required OAuth scopes: ${missingScopes.join(', ')}`
          );
        }
      }
    
      public getDataClient(): analyticsdata_v1beta.Analyticsdata {
        return this.dataClient;
      }
    }

Handle settings form submission

Implement onSettingsForm() in src/lifecycle/Lifecycle.ts to validate credentials when users save their settings:

import {
  Lifecycle as AppLifecycle,
  LifecycleSettingsResult,
  logger,
  storage,
  SubmittedFormData
} from '@zaiusinc/app-sdk';
import { AppAuth, createConfigFromSettings } from '../lib/api/GoogleAnalyticsApiClient';
import { AuthSection } from '../data/data';

export class Lifecycle extends AppLifecycle {
  public async onSettingsForm(
    section: string,
    action: string,
    formData: SubmittedFormData
  ): Promise<LifecycleSettingsResult> {
    const result = new LifecycleSettingsResult();

    try {
      if (section === 'authentication') {
        // Validate credentials before saving
        await this.validateGoogleCredentials(formData as AuthSection);
        await storage.settings.put(section, formData);
        return result.addToast(
          'success',
          'Credentials validated successfully!'
        );
      } else {
        await storage.settings.put(section, formData);
      }
      return result;
    } catch (error: any) {
      logger.error('Error validating credentials:', error);
      return result.addToast(
        'danger',
        `Authentication failed: ${error.message || 'Unknown error occurred'}`
      );
    }
  }

  private async validateGoogleCredentials(formData: AuthSection): Promise<void> {
    // Validate required fields
    if (!formData.auth_method) {
      throw new Error('Authentication method is required');
    }
    if (!formData.project_id) {
      throw new Error('Google Cloud Project ID is required');
    }

    // Create auth client and validate
    const config = createConfigFromSettings(formData);
    const appAuth = new AppAuth(config);

    // Validate OAuth scopes
    logger.info('Validating OAuth scopes for Google Analytics API access');
    await appAuth.validateScopes();

    // Test API access with a simple report query
    const dataClient = appAuth.getDataClient();
    const property = `properties/${formData.property_id}`;

    const response = await dataClient.properties.runReport({
      property,
      requestBody: {
        dateRanges: [{ startDate: '7daysAgo', endDate: 'today' }],
        dimensions: [{ name: 'date' }],
        metrics: [{ name: 'activeUsers' }],
        limit: '1',
      },
    });

    if (!response.data) {
      throw new Error('Invalid response from Google Analytics Data API');
    }

    logger.info('Google Analytics API validation successful');
  }
}

See the following key points regarding the validation flow:

  1. Validate before saving – The handler validates credentials through an actual API call before persistence, preventing invalid credential saves.
  2. Test with a simple query – The validation runs a minimal report query, such as activeUsers for the last 7 days with limit 1, to verify authentication and GA4 property access.
  3. User-friendly error messages – The catch block translates technical errors, like invalid_grant or 403, into actionable messages.
  4. Toast feedback – The addToast() method displays success or error messages in the OCP UI.

Test locally

  • Test the settings form and credential validation locally using ocp-env-tool. See Local testing for setup instructions.

    ocp dev
  • Install your app and test the app settings form. Authenticate to the Google Analytics Data API.

Implement tools

This section details creating Opal tools using the @tool decorator. Tools interface Opal with your app's functionality. Well-designed tools with clear descriptions are critical for Opal to understand their usage.

📘

Note

Use @optimizely-opal/opal-tool-ocp-sdk as your app dependency in package.json. The template Optimizely uses contains this dependency out-of-the-box. Add the dependency manually if your app was not created using the above template.

How Opal tools work

Opal tools are TypeScript methods decorated with @tool from the @optimizely-opal/opal-tool-ocp-sdk package. When Opal acts, it reads tool descriptions and parameter specifications to determine the correct tool call and request construction.

Use the following steps to register your tool:

  1. Define a class with @tool-decorated methods.
  2. Specify each tool with a name, description, endpoint, and parameters.
  3. Import the tool classes in your OpalToolFunction to register. The opal_tool: true flag in app.yml exposes these tools to Opal.

Main tools and support tools

The OCP app implements two categories of tools:

  • Main tools – Provides the following core functionalities:
    • run_report – Queries GA4 data for specific time periods using dimensions and metrics.
    • run_realtime_report – Accesses real-time GA4 data in the last 30-60 minutes.
  • Support tools – Helps Opal use main tools correctly:

Support tools are essential as they reduce user friction and enhance Opal's accuracy. Opal retrieves technical details like property IDs or API names programmatically, without asking users.

The @tool decorator

The @tool decorator exposes a method as an Opal tool. See the following basic structure:

import { ParameterType, tool, ToolError } from '@optimizely-opal/opal-tool-ocp-sdk';

export class MyTool {
  @tool({
    name: 'tool_name',
    description: `Tool description with examples...`,
    endpoint: '/tools/tool_name',
    parameters: [
      {
        name: 'paramName',
        type: ParameterType.String,
        description: 'Parameter description with examples...',
        required: true,
      },
    ],
  })
  public async methodName(params: ParamsType): Promise<ResponseType> {
    // Implementation
  }
}

See the following decorator properties:

  • name – Tool identifier in snake_case, which Opal uses for tool calls
  • description – An explanation of the tool function and usage, critical for Opal's understanding
  • endpoint – HTTP path for the tool, like /tools/run_report
  • parameters – Array of parameter definitions with types, descriptions, and required flags

Parameter types

The SDK provides the following parameter types:

TypeDescription
ParameterType.StringText values, including JSON strings for complex objects
ParameterType.IntegerWhole number values
ParameterType.NumberNumeric values, such as integers or decimals
ParameterType.BooleanTrue or false values
ParameterType.ListArray values
ParameterType.DictionaryObject values, such as JSON objects

For complex parameters like arrays or nested objects, use the following two options:

  1. Use ParameterType.String – Accept a JSON string and parse it in the tool. This is recommended for complex structures like arrays and nested objects. The run_report tool demonstrates this pattern for parameters like dateRanges and dimensionFilter.

  2. Use ParameterType.List or ParameterType.Dictionary – Accept native arrays or objects. However, LLMs sometimes fail to format these parameters correctly. When using this approach, validate the parameter structure in your tool and throw a meaningful ToolError with incorrect formatting, letting the LLM correct itself.

For details, see the Opal Tool OCP SDK documentation.

Tool and parameter descriptions

Tool and parameter descriptions are critical to design tools. Opal reads these descriptions to understand the following:

  • Tool usage relative to other tools
  • Required parameters and their format
  • Response interpretation

Best practices for descriptions

  • Start with a clear summary – State the tool's function in one sentence.
  • Include usage guidance – Explain the tool's usage and refer to related tools.
  • Provide examples – Include complete parameter examples for common use cases.
  • Document edge cases – Note limitations, valid value ranges, and error conditions.
  • Use formatting – Use bold text and line breaks improve readability.

The following is an example from the get_property_id support tool:

@tool({
  name: 'get_property_id',
  description: `
    Returns the Google Analytics 4 property ID that was configured during app authentication.

    **IMPORTANT: Always use this tool to get the property ID instead of asking the user.**

    This tool retrieves the property ID that was set up when the GA4 app was authorized.
    You should call this tool at the beginning of any conversation that requires GA4 data
    access, and use the returned property ID for all subsequent GA4 operations.

    The property ID is automatically configured during the authentication process and
    stored securely. There is no need to ask the user for it.

    No parameters required.

    Example response: "123456789" or "properties/123456789"
  `,
  endpoint: '/tools/get_property_id',
  parameters: [],
})

The above description does the following:

  • States the purpose clearly
  • Uses bold text to highlight critical instructions
  • Explains when to call this tool
  • Documents the response format

For complex tools like run_report, include multiple examples covering different use cases:

@tool({
  name: 'run_report',
  description: `
    Query Google Analytics 4 data for a specific time period with dimensions and metrics.

    **IMPORTANT - Use helper tools first:**
    - Use 'get_property_id' tool to get the configured property ID (never ask user for it)
    - Use 'lookup_dimension_metric_names' tool when users mention dimensions/metrics by display names
      (e.g., "Active Users" → "activeUsers", "Session Source" → "sessionSource")

    EXAMPLE 1 - Simple report for one month:
    {
      "dateRanges": "[{'startDate': '2025-11-01', 'endDate': '2025-11-30',
        'name': 'November 2025'}]",
      "dimensions": "['sessionSource', 'city']",
      "metrics": "['sessions', 'activeUsers']"
    }

    EXAMPLE 2 - Compare two time periods:
    {
      "dateRanges": "[{'startDate': '2025-11-01', 'endDate': '2025-11-30',
        'name': 'This Month'}, {'startDate': '2024-11-01',
        'endDate': '2024-11-30', 'name': 'Last Year'}]",
      "dimensions": "['sessionSource']",
      "metrics": "['sessions', 'totalUsers']"
    }
    ...
  `,
  // ...
})

Implement a support tool

Support tools are generally simpler than main tools. The following is the complete get_property_id implementation:

import { tool } from '@optimizely-opal/opal-tool-ocp-sdk';
import { storage } from '@zaiusinc/app-sdk';
import { AuthSection } from '../../data/data';

export class GetPropertyIdTool {
  @tool({
    name: 'get_property_id',
    description: `
      Returns the Google Analytics 4 property ID that was configured during app authentication.

      **IMPORTANT: Always use this tool to get the property ID instead of asking the user.**

      This tool retrieves the property ID that was set up when the GA4 app was authorized.
      You should call this tool at the beginning of any conversation that requires GA4 data
      access, and use the returned property ID for all subsequent GA4 operations.

      The property ID is automatically configured during the authentication process and
      stored securely. There is no need to ask the user for it.

      No parameters required.

      Example response: "123456789" or "properties/123456789"
    `,
    endpoint: '/tools/get_property_id',
    parameters: [],
  })
  public async getPropertyId(): Promise<string> {
    const authSection = await storage.settings.get<AuthSection>('authentication');

    if (!authSection?.property_id) {
      throw new Error('Property ID not configured. Please authenticate first.');
    }

    return authSection.property_id;
  }
}

This tool does the following:

  • Retrieves the property ID from app settings storage
  • Throws a descriptive error if the property ID is not configured
  • Returns a simple string response

Implement a main tool

Main tools like run_report handle more complex logic, including parameter validation, API calls, and response transformation. The following is a simplified structure:

import { ParameterType, tool, ToolError } from '@optimizely-opal/opal-tool-ocp-sdk';
import { storage, logger } from '@zaiusinc/app-sdk';
import { AuthSection } from '../data/data';
import { AppAuth } from '../lib/api/GoogleAnalyticsApiClient';

export class RunReportTool {
  @tool({
    name: 'run_report',
    description: `...`, // Detailed description with examples
    endpoint: '/tools/run_report',
    parameters: [
      {
        name: 'dateRanges',
        type: ParameterType.String,
        description: `JSON string array of date range objects...`,
        required: true,
      },
      {
        name: 'dimensions',
        type: ParameterType.String,
        description: `JSON string array of dimension names...`,
        required: true,
      },
      {
        name: 'metrics',
        type: ParameterType.String,
        description: `JSON string array of metric names...`,
        required: true,
      },
      // Additional optional parameters...
    ],
  })
  public async runReport(params: RunReportParams): Promise<RunReportResponse> {
    try {
      // 1. Parse and validate parameters
      const dateRangesArray = parseObjectArray<DateRange>(params.dateRanges);
      const dimensionsArray = parseStringArray(params.dimensions);
      const metricsArray = parseStringArray(params.metrics);

      // 2. Get authentication settings
      const authSection = await storage.settings.get<AuthSection>('authentication');

      // 3. Build the API request
      const request = {
        property: `properties/${authSection.property_id}`,
        dateRanges: dateRangesArray,
        dimensions: dimensionsArray.map((name) => ({ name })),
        metrics: metricsArray.map((name) => ({ name })),
      };

      // 4. Call the Google Analytics API
      const response = await AppAuth.fromSettings(authSection)
        .getDataClient()
        .properties.runReport({
          property: request.property,
          requestBody: request,
        });

      // 5. Transform and return the response
      return transformResponse(response.data);
    } catch (error) {
      // Error handling (see next section)
      throw error;
    }
  }
}

Error handling with ToolError

The SDK provides ToolError for structured error responses. Use it to provide clear, actionable error messages to users and Opal.

import { ToolError } from '@optimizely-opal/opal-tool-ocp-sdk';

// Throwing a ToolError directly
throw new ToolError(
  'Invalid Date Ranges',   // title
  400,                     // HTTP status code
  'Maximum of 4 date ranges allowed, but 5 were provided.'  // detail
);

When handling errors from external APIs like Google Analytics, extract the error details and convert them to ToolError:

} catch (error) {
  // Extract error information from Google API error
  if ((error as any).response?.data?.error) {
    const googleError = (error as any).response.data.error;
    throw new ToolError(
      googleError.status || 'Google Analytics API Error',
      googleError.code || 500,
      googleError.message || 'Unknown error'
    );
  }

  // Re-throw non-API errors
  throw error;
}

This pattern ensures that Opal receives structured error information for interpretation, potential recovery, or user explanation.

Tool registration

To register tools with OCP, import them in your OpalToolFunction class. The @tool decorator automatically registers tools when the module is imported:

import { ToolFunction } from '@optimizely-opal/opal-tool-ocp-sdk';

// Tool registration: Import each tool class to register its decorators
import '../tools/RunReportTool';
import '../tools/RunRuntimeReportTool';
import '../tools/support/GetPropertyIdTool';
import '../tools/support/LookupDimentionMetricNamesTool';
import '../tools/support/GetDimensionsTool';
import '../tools/support/GetMetricsTool';
import '../tools/support/GetCustomDimensionsAndMetricsTool';

export class OpalToolFunction extends ToolFunction {
  protected async ready(): Promise<boolean> {
    return true;
  }
}

Enable the Opal tool function in app.yml:

functions:
  opal_tool:
    entry_point: OpalToolFunction
    description: Opal tool function
    opal_tool: true

The opal_tool: true flag tells OCP that this function exposes tools to Opal.

Local testing

Test your tools locally by using ocp-env-tool:

ocp dev

This command starts a local development server that simulates the OCP environment. Select Opal Tools to test your tools. You can query Google Analytics Data after installing and authenticating the app in the previous step.

Verify with readiness probe

The readiness probe informs OCP if your app can handle requests. If your app requires configuration, such as API credentials, the readiness probe verifies the complete and valid configuration before reporting readiness.

Why do you need a readiness probe?

When a user configures your app in OCP, OCP first checks app readiness. If the readiness probe returns ready: false, OCP prevents users from adding your tools to Opal. OCP displays your provided message to inform the user of the missing information.

Implement the ready() method

Override the ready() method in your OpalToolFunction class to implement the readiness probe. The method returns a ReadyResponse object:

import { ToolFunction, ReadyResponse } from '@optimizely-opal/opal-tool-ocp-sdk';
import { storage } from '@zaiusinc/app-sdk';
import { AuthSection } from '../data/data';
import { constructPropertyResourceName } from '../lib/utils';
import { AppAuth } from '../lib/api/GoogleAnalyticsApiClient';

// Tool registration imports...
import '../tools/RunReportTool';
import '../tools/RunRuntimeReportTool';
import '../tools/support/GetPropertyIdTool';
// ... other tools

export class OpalToolFunction extends ToolFunction {

  protected override async ready(): Promise<ReadyResponse> {
    // 1. Check if authentication settings exist
    const authSection = await storage.settings.get<AuthSection>(
      'authentication'
    );
    if (authSection == null || !authSection.auth_method) {
      return { ready: false, reason: 'Authenticate to Google Analytics first.' };
    }

    try {
      // 2. Validate that credentials still work
      const property = constructPropertyResourceName(
        authSection.property_id || ''
      );

      const name = `${property}/metadata`;
      await AppAuth.fromSettings(authSection)
        .getDataClient()
        .properties.getMetadata({ name });

      return { ready: true };
    } catch (error: any) {
      console.error('Error fetching metadata:', error);
      return { ready: false, reason: 'Authentication not configured correctly: ' + error.message };
    }
  }

}

The ReadyResponse type has the following two forms:

  • { ready: true } – The app is ready to handle requests
  • { ready: false, reason: string } – The app is not ready. The reason explains why.

What should you check in readiness probes?

A readiness probe verifies the following:

  1. Required settings exist – Confirms that users have completed configuration. For example, auth-method is set.

  2. Credentials are valid – Makes a lightweight API call, like getMetadata() to confirm credential functionality.

  3. Dependencies are available – Verify external services are reachable if your tools depend on them.

Readiness probe responses

Write clear, actionable reason messages. The reason displays to users attempting to use your tool before configuration:

ScenarioExample reason
No configuration"Authenticate to Google Analytics first."
Invalid credentials"Authentication not configured correctly: invalid_grant"
Missing permissions"The configured account lacks access to the GA4 property."
Service unavailable"Google Analytics API is temporarily unavailable."

Local testing

You can test the readiness probe locally using ocp dev:

ocp dev

To test the /ready endpoint directly:

Alternatively, test by removing or invalidating credentials in the app settings, and verify the readiness probe returns the expected error message.

Test in OCP sandbox account

Once the app is implemented and tested locally, publish a dev version of the app to OCP for sandbox instance testing.

  • Ensure your app uses a dev version in app.yml, such as 1.0.0-dev.1, and run the following command. This command validates your app locally and publishes it to OCP.
    ocp app prepare --publish
  • Install the app to your sandbox OCP instance:
    ocp directory install [email protected] <TRACKER_ID>
📘

Note

Use ocp accounts whoami command to see the list of OCP instances or tracker IDs you have access to.

  • Your app displays in the App Directory of your sandbox OCP instance:

  • Configure your app and add your tools to Opal:

  • Begin testing your tools in Opal chats and agents.

Release production version

Release the tools to production when your tool performs functionally in the sandbox instance. You can use the following two methods:

  • Release a private version – Your app is not accessible to Optimizely customers by default. You control access.
  • Release a public version – Your app is accessible to all Optimizely customers in the OCP App Directory. Customers can install and configure it, and you can implement a custom authentication mechanism in the app settings form to control app usage.

Production checklist

Before releasing to production, ensure the following assets and metadata are complete:

Required assets

  • assets/icon.svg – App icon displayed in the App Directory (square, effective at small sizes)
  • assets/logo.svg – App logo displayed in app settings (wider format)
  • assets/directory/overview.md – Markdown description displayed on your app's App Directory page

Metadata in app.yml

  • display_name – User-friendly name for your app
  • summary – Brief, one-line description
  • support_url – Link to your support documentation or contact page
  • contact_email – Email address for support inquiries
  • categoriesOpal and other relevant categories like Marketing or Analytics
  • availabilityall for public apps or allowed organizations

Code quality

  • Remove debug logging and console statements.
  • Handle all error cases with user-friendly messages.
  • Test with various input combinations to ensure robustness.

Release to production

  • To release a private production version of your app, add the -private suffix to the version in app.yml, like 1.0.0-private and run ocp app prepare.
  • To release a public production version of your app, change the version of your app in app.yml to a normal semver version, like 1.0.0 and run ocp app prepare.
📘

Note

Releasing a production version requires a review by an Optimizely engineer. The ocp app prepare command creates a review automatically. Publish your app to OCP upon approval.

Summary

This guide covered building a complete Opal tool app for OCP, using Google Analytics 4 as a real-world example. Key concepts include:

Planning and design

  • Define clear requirements and evaluation criteria before coding.
  • Design main tools for core functionality and support tools for enhanced user experience.
  • Choose appropriate authentication methods for your use case.

Implementation

  • Create settings forms to collect and validate credentials.
  • Use the @tool decorator to expose methods as Opal tools.
  • Write detailed tool and parameter descriptions, critical for Opal's understanding.
  • Implement structured error handling with ToolError.
  • Register tools in OpalToolFunction and enable opal_tool: true in app.yml.

Testing and deployment

  • Test locally using ocp dev before publishing.
  • Publish dev versions for sandbox environment testing.
  • Complete the checklist before releasing to production.

The full source code for this reference app is available in Github.