Developing a Looker Studio Community Connector

A step-by-step guide by Ralph Keser

Agenda

  • Overview of Looker Studio Connectors
  • Prerequisites & Setup
  • Connector Architecture
  • Building the Connector
  • Testing & Deployment
  • Example: Weather API
  • Q&A

What Are Looker Studio Community Connectors?

Pull data from any API or source into Looker Studio.

Use Cases:

  • Connect to proprietary or less common APIs
  • Customize data retrieval and shape as needed

Prerequisites & Setup

  • Google Account with access to Apps Script
  • Google Apps Script or CLI-based connector dev
  • Familiarity with JavaScript/TypeScript
  • Basic understanding of Looker Studio data model (dimensions, metrics)

Connector Architecture

  • getAuthType() – Defines how users authenticate (NONE, OAUTH2, etc.)
  • getConfig() – Defines user-input parameters (API keys, etc.)
  • getSchema() – Describes data fields (dimensions, metrics, types)
  • getData() – Fetches data from the API, returns rows in the schema

Optional methods: isAdminUser(), isAuthValid(), etc.

 

Step 1: Create Apps Script Project

  1. Go to script.google.com
  2. Create a new project
  3. Rename the project (e.g., “My Looker Studio Connector”)
  4. You’ll have a default Code.gs file
  5. Show and edit appsscript.json as needed
				
					{
  "timeZone": "Europe/Berlin",
  "dependencies": {
  },
  "dataStudio": {
    "name": "Weather Api (Diconium)",
    "company": "Ralph",
    "logoUrl": "https://24898528.fs1.hubspotusercontent-eu1.net/hub/24898528/hubfs/2024%20theme%20assets/Layer%201.png?width=594&height=344&name=Layer%201.png",
    "addonUrl": "https://www.ataraxie.de",
    "supportUrl": "https://www.ataraxie.de",
    "description": "Looker Weather connector"
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/script.external_request"
  ],
  "urlFetchWhitelist": [
    "https://weather.visualcrossing.com/",
    "https://archive-api.open-meteo.com/"
    
  ],
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8"
}
				
			

Step 2: Define Authentication

Simple example for no-auth connectors. If your API requires OAuth2 or API keys, configure accordingly.

				
					// https://developers.google.com/datastudio/connector/reference#getauthtype
function getAuthType() {
  return { type: 'NONE' }; // No authentication required
}
				
			

Step 3: Configure User Inputs

Define input fields for your connector’s UI via getConfig().

				
					function getConfig() {
  var config = cc.getConfig();

  config.newTextInput()
    .setId("latitude")
    .setName("Latitude")
    .setHelpText("Enter latitude (-90 to 90), e.g., 53.5511 for Hamburg")
    .setPlaceholder("53.5511");

  config.newTextInput()
    .setId("longitude")
    .setName("Longitude")
    .setHelpText("Enter longitude (-180 to 180), e.g., 9.9937 for Hamburg")
    .setPlaceholder("9.9937");

  var timezones = [
    ['Auto', 'auto'], ['UTC', 'UTC'], ['Europe/London', 'Europe/London'],
    ['Europe/Berlin', 'Europe/Berlin'], ['America/New_York', 'America/New_York'],
    ['America/Chicago', 'America/Chicago'], ['America/Los_Angeles', 'America/Los_Angeles'],
    ['Asia/Tokyo', 'Asia/Tokyo']
  ];

  var tzSelect = config.newSelectSingle().setId("timezone").setName("Timezone");
  timezones.forEach(function (tz) {
    tzSelect.addOption(config.newOptionBuilder().setLabel(tz[0]).setValue(tz[1]));
  });

  return config.build();
}
				
			

Step 4: Define the Schema

The schema lists each column’s name, label, data type, and usage (DIMENSION vs. METRIC). Ensure names match how you populate values in getData().

				
					function getSchema() {
  return {
    schema: [
      { 
        name: "date", 
        label: "Date", 
        dataType: "STRING",
        semantics: {
          conceptType: "DIMENSION",
          semanticType: "YEAR_MONTH_DAY"
        }
      },
      { name: "temperature_2m_max", label: "Max Temperature (°C)", dataType: "NUMBER" },
      { name: "temperature_2m_min", label: "Min Temperature (°C)", dataType: "NUMBER" },
      { name: "temperature_2m_mean", label: "Mean Temperature (°C)", dataType: "NUMBER" },
      { name: "relative_humidity_2m_mean", label: "Mean Humidity (%)", dataType: "NUMBER" },
      { name: "precipitation_sum", label: "Precipitation (mm)", dataType: "NUMBER" },
      { name: "wind_speed_10m_max", label: "Max Wind Speed (km/h)", dataType: "NUMBER" },
      { name: "wind_direction_10m_dominant", label: "Dominant Wind Direction (°)", dataType: "NUMBER" }
    ]
  };
}

				
			

Step 5: Fetch & Return Data

  • Ensure the order of values matches the schema
  • Convert arrays/objects to strings if necessary
  • Handle null or missing values gracefully
				
					
function getData(request) {
  var config = request.configParams;
  var lat = config.latitude, lon = config.longitude, tz = config.timezone || 'auto';

  // Validate coordinates
  if (!lat || !lon || isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
    cc.newUserError()
      .setDebugText('Invalid coordinates')
      .setText('Please provide valid latitude (-90 to 90) and longitude (-180 to 180)')
      .throwException();
  }

  // Process fields and dates
  var requestedFields = request.fields || [];
  var fieldNames = requestedFields.map(function (f) { return f.name; });
  var schema = getSchema().schema.filter(function (f) { return fieldNames.indexOf(f.name) !== -1; });
  schema.sort(function (a, b) { return fieldNames.indexOf(a.name) - fieldNames.indexOf(b.name); });

  var startDate, endDate;
  if (request.dateRange && request.dateRange.startDate && request.dateRange.endDate) {
    startDate = formatDateForAPI(request.dateRange.startDate);
    endDate = formatDateForAPI(request.dateRange.endDate);
  } else {
    var today = new Date();
    var pastDate = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
    startDate = Utilities.formatDate(pastDate, 'UTC', 'yyyy-MM-dd');
    endDate = Utilities.formatDate(today, 'UTC', 'yyyy-MM-dd');
  }

  // Build API URL
  var params = {
    latitude: lat, longitude: lon, start_date: startDate, end_date: endDate,
    daily: 'temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max,wind_direction_10m_dominant',
    timezone: tz
  };

  var url = 'https://archive-api.open-meteo.com/v1/archive?' +
    Object.keys(params).map(function (k) { return k + '=' + encodeURIComponent(params[k]); }).join('&');

  var weatherData;
  try {
    var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    if (response.getResponseCode() !== 200) {
      throw new Error('API error: ' + response.getResponseCode());
    }

    weatherData = JSON.parse(response.getContentText());
    if (!weatherData.daily || !weatherData.daily.time) {
      throw new Error('No data available');
    }

    Logger.log('Data fetched from API');

  } catch (error) {
    cc.newUserError()
      .setDebugText('API Error: ' + error.toString())
      .setText('Unable to fetch weather data. Check coordinates and try again.')
      .throwException();
  }

  // Process data efficiently
  var daily = weatherData.daily;
  var fieldMap = {
    'date': daily.time,
    'temperature_2m_max': daily.temperature_2m_max,
    'temperature_2m_min': daily.temperature_2m_min,
    'temperature_2m_mean': daily.temperature_2m_mean,
    'relative_humidity_2m_mean': daily.relative_humidity_2m_mean,
    'precipitation_sum': daily.precipitation_sum,
    'wind_speed_10m_max': daily.wind_speed_10m_max,
    'wind_direction_10m_dominant': daily.wind_direction_10m_dominant
  };

  var rows = daily.time.map(function (_, i) {
    return {
      values: schema.map(function (field) {
        var data = fieldMap[field.name];
        if (field.name === 'date') {
          // Format date as YYYYMMDD for Looker Studio date range control
          var dateStr = String(data[i] || '');
          return dateStr.replace(/-/g, '');
        }
        return (data && data[i] !== undefined ? data[i] : null);
      })
    };
  });

  Logger.log('Processed ' + rows.length + ' rows');
  return { schema: schema, rows: rows };
}
				
			

Step 6: Testing Your Connector

  1. Use Logger.log() in Apps Script to debug
  2. In Looker Studio:
    • Create a new data source → Select “Build Your Own” → Choose your connector
    • Enter config parameters (API key, etc.) → “Connect”
    • Verify fields → “Create Report” to visualize

Deployment

  1. Deploy in Apps Script: Click “Deploy” → “New Deployment” → Type: “Web app”
  2. Set access level (e.g., “Anyone” if no auth required)
  3. Share the deployment URL for others to use

Common Pitfalls

  • Schema vs. Rows mismatch (field count must match)
  • Data type mismatches (STRING vs. NUMBER)
  • Missing/wrong Auth if API requires credentials
  • Caching – Looker Studio may cache data

Best Practices

  • Error Handling: Return readable error messages if API fails
  • Refactor: Keep config, schema, and data logic organized
  • Logging: Use Logger.log() for debugging
  • Documentation: Provide usage instructions, parameters, rate limits

Resources

Example: Weather API (Visual Crossing)

Connector Parameters:

  • API Key: Required for authentication with the Visual Crossing Weather API.
  • Location: City or latitude/longitude.
  • Start and End Dates: Range to query.

Example Request:

https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline/Hamburg/2025-01-01/2025-01-03?unitGroup=metric&include=days&key=LMGNFA7QYWBT2Z4G38UB89L79&contentType=json

Q&A

Thank you for attending!

Example: Weather API (Open-Meteo)

Connector Parameters:

  • Latitude: e.g. 53.5511 for Hamburg
  • Longitude: e.g. 9.9937 for Hamburg
  • Timezone: auto, UTC, or IANA names like Europe/Berlin
  • Date range handled via Looker Studio’s picker; defaults to last 30 days

Example Request:

https://archive-api.open-meteo.com/v1/archive?latitude=53.5511&longitude=9.9937&start_date=2025-06-01&end_date=2025-06-15&daily=temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max

 

				
					// Looker Studio Plugin for Open-Meteo Weather API
// Community Connector implementation for Google Data Studio

var cc = DataStudioApp.createCommunityConnector();

// https://developers.google.com/datastudio/connector/reference#getauthtype
function getAuthType() {
  return { type: 'NONE' }; // No authentication required
}

function isAdminUser() {
  return true; // Allow all users to act as admin
}

function getConfig() {
  var config = cc.getConfig();

  config.newTextInput()
    .setId("latitude")
    .setName("Latitude")
    .setHelpText("Enter latitude (-90 to 90), e.g., 53.5511 for Hamburg")
    .setPlaceholder("53.5511");

  config.newTextInput()
    .setId("longitude")
    .setName("Longitude")
    .setHelpText("Enter longitude (-180 to 180), e.g., 9.9937 for Hamburg")
    .setPlaceholder("9.9937");

  var timezones = [
    ['Auto', 'auto'], ['UTC', 'UTC'], ['Europe/London', 'Europe/London'],
    ['Europe/Berlin', 'Europe/Berlin'], ['America/New_York', 'America/New_York'],
    ['America/Chicago', 'America/Chicago'], ['America/Los_Angeles', 'America/Los_Angeles'],
    ['Asia/Tokyo', 'Asia/Tokyo']
  ];

  var tzSelect = config.newSelectSingle().setId("timezone").setName("Timezone");
  timezones.forEach(function (tz) {
    tzSelect.addOption(config.newOptionBuilder().setLabel(tz[0]).setValue(tz[1]));
  });

  return config.build();
}

function getSchema() {
  return {
    schema: [
      { 
        name: "date", 
        label: "Date", 
        dataType: "STRING",
        semantics: {
          conceptType: "DIMENSION",
          semanticType: "YEAR_MONTH_DAY"
        }
      },
      { name: "temperature_2m_max", label: "Max Temperature (°C)", dataType: "NUMBER" },
      { name: "temperature_2m_min", label: "Min Temperature (°C)", dataType: "NUMBER" },
      { name: "temperature_2m_mean", label: "Mean Temperature (°C)", dataType: "NUMBER" },
      { name: "relative_humidity_2m_mean", label: "Mean Humidity (%)", dataType: "NUMBER" },
      { name: "precipitation_sum", label: "Precipitation (mm)", dataType: "NUMBER" },
      { name: "wind_speed_10m_max", label: "Max Wind Speed (km/h)", dataType: "NUMBER" },
      { name: "wind_direction_10m_dominant", label: "Dominant Wind Direction (°)", dataType: "NUMBER" }
    ]
  };
}

// Helper function to format date for API (Open-Meteo uses YYYY-MM-DD)
function formatDateForAPI(dateString) {
  return dateString && dateString.length === 8 ?
    dateString.substring(0, 4) + '-' + dateString.substring(4, 6) + '-' + dateString.substring(6, 8) :
    dateString;
}

function getData(request) {
  var config = request.configParams;
  var lat = config.latitude, lon = config.longitude, tz = config.timezone || 'auto';

  // Validate coordinates
  if (!lat || !lon || isNaN(lat) || isNaN(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
    cc.newUserError()
      .setDebugText('Invalid coordinates')
      .setText('Please provide valid latitude (-90 to 90) and longitude (-180 to 180)')
      .throwException();
  }

  // Process fields and dates
  var requestedFields = request.fields || [];
  var fieldNames = requestedFields.map(function (f) { return f.name; });
  var schema = getSchema().schema.filter(function (f) { return fieldNames.indexOf(f.name) !== -1; });
  schema.sort(function (a, b) { return fieldNames.indexOf(a.name) - fieldNames.indexOf(b.name); });

  var startDate, endDate;
  if (request.dateRange && request.dateRange.startDate && request.dateRange.endDate) {
    startDate = formatDateForAPI(request.dateRange.startDate);
    endDate = formatDateForAPI(request.dateRange.endDate);
  } else {
    var today = new Date();
    var pastDate = new Date(today.getTime() - (30 * 24 * 60 * 60 * 1000));
    startDate = Utilities.formatDate(pastDate, 'UTC', 'yyyy-MM-dd');
    endDate = Utilities.formatDate(today, 'UTC', 'yyyy-MM-dd');
  }

  // Build API URL
  var params = {
    latitude: lat, longitude: lon, start_date: startDate, end_date: endDate,
    daily: 'temperature_2m_max,temperature_2m_min,temperature_2m_mean,relative_humidity_2m_mean,precipitation_sum,wind_speed_10m_max,wind_direction_10m_dominant',
    timezone: tz
  };

  var url = 'https://archive-api.open-meteo.com/v1/archive?' +
    Object.keys(params).map(function (k) { return k + '=' + encodeURIComponent(params[k]); }).join('&');

  var weatherData;
  try {
    var response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
    if (response.getResponseCode() !== 200) {
      throw new Error('API error: ' + response.getResponseCode());
    }

    weatherData = JSON.parse(response.getContentText());
    if (!weatherData.daily || !weatherData.daily.time) {
      throw new Error('No data available');
    }

    Logger.log('Data fetched from API');

  } catch (error) {
    cc.newUserError()
      .setDebugText('API Error: ' + error.toString())
      .setText('Unable to fetch weather data. Check coordinates and try again.')
      .throwException();
  }

  // Process data efficiently
  var daily = weatherData.daily;
  var fieldMap = {
    'date': daily.time,
    'temperature_2m_max': daily.temperature_2m_max,
    'temperature_2m_min': daily.temperature_2m_min,
    'temperature_2m_mean': daily.temperature_2m_mean,
    'relative_humidity_2m_mean': daily.relative_humidity_2m_mean,
    'precipitation_sum': daily.precipitation_sum,
    'wind_speed_10m_max': daily.wind_speed_10m_max,
    'wind_direction_10m_dominant': daily.wind_direction_10m_dominant
  };

  var rows = daily.time.map(function (_, i) {
    return {
      values: schema.map(function (field) {
        var data = fieldMap[field.name];
        if (field.name === 'date') {
          // Format date as YYYYMMDD for Looker Studio date range control
          var dateStr = String(data[i] || '');
          return dateStr.replace(/-/g, '');
        }
        return (data && data[i] !== undefined ? data[i] : null);
      })
    };
  });

  Logger.log('Processed ' + rows.length + ' rows');
  return { schema: schema, rows: rows };
}

function testApiCall() {
  return getData({
    configParams: { latitude: "53.5511", longitude: "9.9937", timezone: "Europe/Berlin" },
    dateRange: { startDate: "20250101", endDate: "20250112" },
    fields: [{ name: "date" }, { name: "temperature_2m_max" }, { name: "temperature_2m_min" }]
  });
}