// v0.29 2025-09-10 // connect-change-or-delete-activities-by-device function cancel() { window.localStorage.setItem("_connect_activities_cancel", 1); } function connectActivities(deviceArg, operationArg) { 'use strict'; window.localStorage.removeItem("_connect_activities_cancel"); class ShowUsageError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } class CancellationError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } function replaceAll(str, find, replace) { return str.replace(new RegExp(find, 'g'), replace); } // https://stackoverflow.com/a/41817778 // https://stackoverflow.com/questions/9627646/chrome-javascript-developer-console-is-it-possible-to-call-console-log-withou/41817778#41817778 var virtualConsole = { lines: [], currentLine: 0, subs: {}, setSub: function(key, value) { virtualConsole.subs[key] = value; }, log: function (msg, appendToCurrentLine) { if (!appendToCurrentLine) virtualConsole.currentLine++; if (appendToCurrentLine && virtualConsole.lines[virtualConsole.currentLine]) { virtualConsole.lines[virtualConsole.currentLine] += msg; } else { virtualConsole.lines[virtualConsole.currentLine] = msg; } console.clear(); virtualConsole.lines.forEach(function (line) { for (const key in virtualConsole.subs) [ line = replaceAll(line, key, virtualConsole.subs[key]) ] console.log(line); }); }, clear: function () { console.clear(); virtualConsole.currentLine = 0; } } async function fetchRequest(description, url, method, data, requestIsJson) { const shouldCancel = window.localStorage.getItem("_connect_activities_cancel"); if (shouldCancel) { window.localStorage.removeItem("_connect_activities_cancel"); throw new CancellationError('cancelled by user'); } let access_token try { const tokenStorage = window.localStorage.getItem("token"); access_token = JSON.parse(tokenStorage).access_token; } catch (e) { throw new Error(`could not get access token. Are you logged into https://connect.garmin.com?`) } const headers = new Headers(); if (requestIsJson) { headers.append("Content-Type", "application/json") } headers.append("NK", "NT"); headers.append("Di-Backend", "connectapi.garmin.com"); headers.append("Authorization", `Bearer ${access_token}`); let response try { response = await fetch(url, { method: method || 'GET', headers: headers, body: data || null, }) } catch (error) { throw new Error(`failed to ${description}: ${error}`) } if (!response.ok) { throw new Error(`failed to ${description}: ${response.status} ${response.statusText}`) } return response; } function logCss(str) { const count = (str.match(/%c/g) || []).length; const extraArgs = []; let i for (i = 0; i < count; i += 2) { extraArgs.push('color: #32CD32'); extraArgs.push('default'); } if (i < count) { extraArgs.push('color: #32CD32'); } console.log(str, ...extraArgs); } const maxLimit = 10000; let operation = undefined; const options = { limit: maxLimit, device: undefined, ignoreMissingDevices: false, name: undefined, at: undefined, after: undefined, before: undefined, startingAt: undefined, endingAt: undefined, type: undefined, newType: undefined, }; const options_desc = { limit: 'NUMBER,\n // activity limit (applied before date/device filters (e.g. 20)\n', device: "NUMBER,\n // device ID (e.g. 1234)", ignoreMissingDevices: 'true or false,\n // false by default. When true, the script will *not* try to fetch devices IDs that are missing from the activity list. This can speed up the activity listing significantly\n', name: 'TEXT,\n // match activities by name (e.g. "hik" matches activities named "Hiking", "Vancouver Hiking", etc.)\n', at: 'DATE,\n // match activities at date (e.g. "2025-03-31")', after: 'DATE,\n // match activities after date', before: 'DATE,\n // match activities before date', startingAt: 'DATE,\n // match activities starting at date', endingAt: 'DATE,\n // match activities ending at date\n', type: 'NUMBER or NAME,\n // activity type to match (e.g. 17 or "running", 9 or "walking")', newType: 'NUMBER or NAME,\n // new activity type for "change-type" operation', } const knownOptions = Object.keys(options); const knownOps = ['show', 'delete', 'change-type', 'show-types', 'help']; function usage() { logCss( ` Show Advanced Usage: %cconnectActivities("help")%c Basic Usage: OPERATION = ${knownOps.map(op => `"${op}"`)} • Delete activities matching DEVICE_ID: %cconnectActivities(DEVICE_ID, "delete")%c • Show activities matching DEVICE_ID: %cconnectActivities(DEVICE_ID, "show")%c • Show activities from any device: %cconnectActivities("show")%c • Change the type of activities from device 1234 to walking: %cconnectActivities("change-type", {device: 1234, newType: "walking"})%c • Show all activity types for use with "change-type" operation or type filter %cconnectActivities("show-types")%c DEVICE_ID can be found on your watch as follows: 1) From the watchface, open the menu (hold UP on most watches) 2) Depending on the watch, select: System > About Settings > System > About Watch Settings > System > About 3) Take note of the Unit ID on the first page - it's a number Example: %cconnectActivities(1234, "delete")%c `); } function advanced_usage() { usage(true); logCss( ` -- Advanced usage: %cconnectActivities(OPERATION, {OPTIONS})%c OPERATION = ${knownOps.map(op => `%c"${op}"%c`)} {OPTIONS} = { ${knownOptions.map(op => `%c${op}%c: ${options_desc[op]}`) .reduce((acc, val) => { return acc + ` ${val}\n` }, "") }} • Change the type of all walking activities to running: %cconnectActivities("change-type", { type: "walking", newType: "running" })%c • Show all running activities (where the activity type is 'running'): %cconnectActivities("show", { type: "running" })%c • Show all activities whose name contains "hik" like "Hiking" or "Vancouver Hiking": %cconnectActivities("show", { name: "hik" })%c • Show last 10 activities from any device (NOTE: limit is applied *before* any device or date filtering): %cconnectActivities("show", { limit: 10 })%c • Show activities for 2025-03-31: %cconnectActivities("show", { at: "2025-03-31" })%c • Show activities for March 2025: %cconnectActivities("show", { startingAt: "2025-03-01", endingAt: "2025-03-31" })%c • Show activities from device ID 1234, for March 2025: %cconnectActivities("show", { device: 1234, startingAt: "2025-03-01", endingAt: "2025-03-31" })%c • Delete activities from device ID 1234, for March 2025: %cconnectActivities("delete", { device: 1234, startingAt: "2025-03-01", endingAt: "2025-03-31" })%c • Delete activities from device ID 1234, after (but not including) 2025-03-31: %cconnectActivities("delete", { device: 1234, after: "2025-03-31" })%c • Delete activities from device ID 1234, before (but not including) 2025-03-31: %cconnectActivities("delete", { device: 1234, before: "2025-03-31" })%c • Delete activities from device ID 1234, starting at (and including) 2025-03-31: %cconnectActivities("delete", { device: 1234, startingAt: "2025-03-31" })%c • Delete activities from device ID 1234, ending at (and including) 2025-03-31: %cconnectActivities("delete", { device: 1234, endingAt: "2025-03-31" })%c `); } function parseDate(a, matchStart) { const dateRegExp = /^(\d\d\d\d)-(\d\d)-(\d\d)/; const match = a.match(dateRegExp); if (!match || match.length == 0) { return undefined; } if (!matchStart && a != match[0]) { return undefined; } return match[0]; } function parseDateArg(a, argName) { if (a === undefined) { return a; } if (typeof a === "string") { const result = parseDate(a); if (result) return result } console.error(`'${argName}' option must be a date in the form YYYY-MM-DD (e.g. "2025-05-31")`); throw new Error("") } let commandLine = null; async function main() { // parse options try { // old style if (typeof operationArg === "string") { if (operationArg === 'help') { advanced_usage(); return; } options.device = deviceArg; operation = operationArg; commandLine = `connectActivities(${deviceArg}, "${operationArg}")`; } else if (typeof deviceArg === "string") { if (deviceArg === 'help') { advanced_usage(); return; } operation = deviceArg; commandLine = `connectActivities("${operation}")`; if (typeof operationArg === 'object' && operationArg !== null && !Array.isArray(operationArg)) { const userOptions = operationArg commandLine = `connectActivities("${operation}", ${JSON.stringify(userOptions)})`; const unknownKeys = Object.keys(userOptions).filter(key => !knownOptions.includes(key)) if (unknownKeys.length) { console.error(`Unknown option: ${unknownKeys[0]}`) console.log(`Supported options: ${knownOptions}\n`) throw new ShowUsageError(); } Object.assign(options, userOptions) if (typeof options.limit === "number") { options.limit = options.limit < maxLimit ? options.limit : maxLimit; } else if (options.limit !== undefined) { console.error("invalid limit. Limit must be a number (e.g. 20)"); throw new ShowUsageError(); } if (typeof options.name !== "string" && typeof options.name !== "undefined") { console.error('invalid name. Name must be a string - e.g. "run"'); throw new ShowUsageError(); } if (typeof options.device !== "number" && options.device !== undefined) { console.error("invalid device ID. Device ID must be a number (e.g. 1234)"); throw new ShowUsageError(); } if (typeof options.newType !== "number" && typeof options.newType !== "string" && options.newType !== undefined) { console.error('invalid new activity type (newType). Activity type must be a number or name (e.g. 9 or "walking")'); throw new ShowUsageError(); } if (typeof options.type !== "number" && typeof options.type !== "string" && options.type !== undefined) { console.error('invalid activity type (type). Activity type must be a number or name (e.g. 9 or "walking")'); throw new ShowUsageError(); } if (options.type === options.newType && options.newType !== undefined) { console.error('Activity type filter cannot be the same as new activity type') throw new ShowUsageError(); } options.at = parseDateArg(options.at, "at"); options.before = parseDateArg(options.before, "before"); options.after = parseDateArg(options.after, "after"); options.startingAt = parseDateArg(options.startingAt, "startingAt"); options.endingAt = parseDateArg(options.endingAt, "endingAt"); if (options.at) { if (options.before || options.after || options.startingAt || options.endingAt) { console.error("'at' option cannot be specified with any of 'before', 'after', 'startingAt', or 'endingAt' options"); throw new ShowUsageError(); } options.startingAt = options.endingAt = options.at; } if (options.after && options.startingAt) { console.error("Cannot specify 'after' and 'startingAt' options at the same time") throw new ShowUsageError(); } if (options.before && options.endingAt) { console.error("Cannot specify 'before' and 'endingAt' options at the same time") throw new ShowUsageError(); } } } else { throw new ShowUsageError(); } if (operation === "showAll") { operation = "show"; } if (typeof operation === 'string') { if (!knownOps.includes(operation)) { console.error(`Unknown operation: ${operation}`); throw new ShowUsageError(); } } else { console.error('Missing operation'); throw new ShowUsageError(); } if (operation === 'change-type' && (typeof options.newType != 'number' && typeof options.newType != 'string')) { console.error('newType option is required for "change-type" operation'); throw new ShowUsageError(); } if (typeof options.device === 'undefined') { if (operation === 'delete') { console.error("Delete operation requires a device ID") throw new ShowUsageError(); } } else if (typeof options.device !== 'number') { console.error('Device ID must be a number') throw new ShowUsageError(); } else if (options.device < 0) { console.error('Device ID must not be a negative number') throw new ShowUsageError(); } } catch(e) { if (e instanceof ShowUsageError) { usage(); return; } throw e } if (!window.location.href.startsWith('https://connect.garmin.com/')) { throw new Error('this script must be run from https://connect.garmin.com') } let activityTypes let resolvedNewActivityTypeKey let resolvedNewActivityTypeId let resolvedTypeId let resolvedTypeKey if (operation === 'show-types') { activityTypes = await (await fetchRequest('fetch activity list', 'https://connect.garmin.com/activity-service/activity/activityTypes')).json(); console.log(`[\n${ activityTypes.map(type => ` "${type.typeKey}": ${type.typeId},\n`) .reduce((acc, val) => acc + val, "") }]`) return } else if (operation === 'change-type' || options.type !== undefined) { activityTypes = await (await fetchRequest('fetch activity list', 'https://connect.garmin.com/activity-service/activity/activityTypes')).json(); for (const type of activityTypes) { if (type.isHidden || type.restricted) { continue; } if (options.newType === type.typeId || options.newType === type.typeKey) { resolvedNewActivityTypeKey = type.typeKey resolvedNewActivityTypeId = type.typeId } if (options.type === type.typeId || options.type === type.typeKey) { resolvedTypeId = type.typeId resolvedTypeKey = type.typeKey } } if (options.newType !== undefined && resolvedNewActivityTypeId === undefined) { console.error(`Activity type doesn't exist: ${options.newType}`) logCss( `Type the following line to see all activity types: %cconnectActivities("show-types")%c `); return; } if (options.type !== undefined && resolvedTypeId === undefined) { console.error(`Activity type doesn't exist: ${options.type}`) logCss( `Type the following line to see all activity types: %cconnectActivities("show-types")%c `); return; } if (resolvedNewActivityTypeId === resolvedTypeId) { console.error(`Activity type filter "${resolvedTypeKey}" (${resolvedTypeId}) is the same as new type "${resolvedNewActivityTypeKey}" (${resolvedNewActivityTypeId}) - nothing to do` ) return false; } } if (operation !== 'show') { console.log(`⚠️ You will be prompted before any activities will be ${operation === 'delete' ? 'deleted' : 'changed'}`); } console.log("⏳ Fetching activity list (this might take a while)..."); const activityListUrl = `https://connect.garmin.com/activitylist-service/activities/search/activities?limit=${options.limit}&start=0` const activities = await (await fetchRequest('fetch activity list', activityListUrl)).json(); if (activities.length === 0) { console.log('No activities found') return; } console.log(`Found %c${activities.length}%c activities in total (after limit, before filtering)`, 'color: #32CD32', 'default'); let numMatches = 0; let numErrors = 0; // filter by date, type and name before the v expensive operation of getting device IDs const filteredActivities = activities.filter(activity => { if (typeof options.name === 'string') { if (!activity.activityName.toLowerCase().includes(options.name.toLowerCase())) { return false; } } if (resolvedTypeId !== undefined) { if (activity.activityType.typeId !== resolvedTypeId) { return false; } } const activityDate = parseDate(activity.startTimeLocal, true); // console.log(`activityDate = ${activityDate}`); if (!activityDate && (options.before || options.after || options.startingAt || options.endingAt)) { console.warn(`Skipping activity ${activityId} because start time could not be determined and one or more of the following options was specified: before, after, startingAt, endingAt`); return false; } if (options.before && activityDate >= options.before) { return false; } if (options.endingAt && activityDate > options.endingAt) { return false; } if (options.after && activityDate <= options.after) { return false; } if (options.startingAt && activityDate < options.startingAt) { return false; } return true; }); const activitiesWithMissingDeviceId = filteredActivities.filter(activity => typeof activity.deviceId !== 'number'); if (activitiesWithMissingDeviceId.length) { if (options.ignoreMissingDevices) { console.warn( `⚠️ ${activitiesWithMissingDeviceId.length} device IDs are missing from the activity list (after filtering), but the ignoreMissingDevices options was specified.` ) } else { virtualConsole.log( `⚠️ ${activitiesWithMissingDeviceId.length} device IDs are missing from the activity list (after filtering). Please wait while we load missing device IDs individually. ❗ This may take a *long* time❗ • To ignore missing device IDs, re-run this script with the option 'ignoreMissingDevices: true' e.g. connectActivities('show', { ignoreMissingDevices: true}); • To cancel this operation... ...close the original Connect browser tab, not this dev tools tray / window. or ...type the following line and press ENTER/RETURN: cancel() `); let numProcessed = 0; let hourglasses = ['⏳', '⏳', '⌛', '⌛']; virtualConsole.setSub('%HOURGLASS%', hourglasses[numProcessed % hourglasses.length]); virtualConsole.setSub('%CURRENT%', numProcessed); virtualConsole.log(`%HOURGLASS% Loading IDs [%CURRENT%/${activitiesWithMissingDeviceId.length}]\n`, true); for (const activity of activitiesWithMissingDeviceId) { // console.log(`Activity with ID ${a.activityId} is missing device ID. Attempting to retrieve device ID`); let metadata = undefined; try { metadata = await (await fetchRequest('fetch activity metadata', `https://connect.garmin.com/activity-service/activity/${activity.activityId}`)).json(); } catch (e) { if (e instanceof CancellationError) { throw e; } virtualConsole.log('\n', true); virtualConsole.log(e, true); virtualConsole.log('\n', true); } if (metadata) { try { activity.deviceId = parseInt(metadata.metadataDTO.deviceMetaDataDTO.deviceId, 10); } catch (e) { virtualConsole.log('\n', true); virtualConsole.log(e, true); virtualConsole.log('\n', true); } } numProcessed++; virtualConsole.setSub('%HOURGLASS%', hourglasses[numProcessed % hourglasses.length]); virtualConsole.setSub('%CURRENT%', numProcessed); virtualConsole.log('.', true); } console.log(''); } } let numSkipped = 0; const activitiesToMod = []; for (const a of filteredActivities) { const deviceId = a.deviceId; if (options.device === undefined || (deviceId === options.device && typeof deviceId === "number")) { console.log( `• 📅 ${a.startTimeLocal} | ID: ${a.activityId} | device: ${deviceId === undefined ? 'missing' : deviceId} | type: ${a.activityType.typeKey} (${a.activityType.typeId}) ✏️ ${a.activityName || ''} 🔗 https://connect.garmin.com/modern/activity/${a.activityId}`) numMatches++; if (operation === 'change-type') { if (a.activityType.typeId === resolvedNewActivityTypeId) { console.log(` ⚠️ Activity ${a.activityId}'s type is already ${resolvedNewActivityTypeKey}. Skipping activity`); numSkipped++; continue; } } if (operation !== 'show') { activitiesToMod.push(a); } } } if (activitiesToMod.length) { let operationDesc = 'delete'; if (operation === 'change-type') { operationDesc = 'change'; } console.warn(`Found ${activitiesToMod.length} activities to ${operationDesc.toLowerCase()}`); console.warn('Switch back to your Connect browser tab to confirm this operation') if (!confirm( `⚠️ Are you sure you want to ${operationDesc} ${activitiesToMod.length} activities? 🚨 This CANNOT be undone 🚨 To cancel while in progress: • close *this* tab, *not* the dev tools or • type the following line and press ENTER/RETURN: cancel()`)) { console.log('operation cancelled') return; } else { console.log('operation confirmed') } } for (const activity of activitiesToMod) { const id = activity.activityId if (operation === 'delete') { console.log(` ⚠️ Deleting activity ${id}...`) try { await fetchRequest('delete activity', `https://connect.garmin.com/activity-service/activity/${id}`, 'DELETE') console.log(` ❗ Deleted activity ${id}`); } catch (e) { numErrors++; console.error(e); } } else if (operation === 'change-type') { console.log(` ⚠️ Changing the type of activity ${id} to ${resolvedNewActivityTypeKey}...`) try { const payload = { "activityId": id, "activityTypeDTO": { "nameKey": `activity_type_${resolvedNewActivityTypeKey}`, "value": resolvedNewActivityTypeId, "typeKey": resolvedNewActivityTypeKey, "restricted": false, // "displayOrder": 4 } } await fetchRequest('change activity type', `https://connect.garmin.com/activity-service/activity/${id}`, 'PUT', JSON.stringify(payload), true) console.log(` ❗ Changed type for activity ${id} to ${resolvedNewActivityTypeKey}`); } catch (e) { numErrors++; console.error(e) } } } return { numMatches, numErrors, numSkipped, numActivities: activities.length, } } main().then( (result) => { if (typeof result !== 'object') { return; } const { numMatches, numErrors, numSkipped, numActivities } = result; let operationString = 'showed'; let operationStringForError = 'show'; switch (operation) { case 'delete': { operationString = 'deleted' operationStringForError = 'delete' break } case 'change-type': { operationString = 'changed the type of' operationStringForError = 'change the type of' } } console.log(`Found %c${numActivities}%c activities in total (after limit, before filtering)`, 'color: #32CD32', 'default'); console.log(`Filters matched %c${numMatches}%c activities`, 'color: #32CD32', 'default') if (operation !== 'show') { console.log(`Successfully ${operationString} %c${numMatches - numErrors- numSkipped}%c activities`, 'color: #32CD32', 'default') } if (numSkipped) { console.log(`Skipped %c${numSkipped}%c activities`, 'color: yellow', 'default') } if (numErrors) { console.log(`Failed to ${operationStringForError} %c${numErrors}%c activities`, 'color: red', 'default') } commandLine && logCss(`To rerun this script with the same options, press ⬆️ or type:\n\n%c${commandLine}%c`); }, (e) => { console.error(e); commandLine && logCss(`To rerun this script with the same options, press ⬆️ or type:\n\n%c${commandLine}%c`); } ) } console.log(`For help, type: %cconnectActivities() `, 'color: #32CD32')