Repository: elastic/synthetics (ts)

Issue:
Make tags behave consistently
There has been some confusion over the current implementation of tags because it differs from what is expected (for example as raised in https://github.com/elastic/synthetics/issues/775).

Synthetics needs to be updated to consolidate how `tags` (and `match` in the CLI) work, as follows:
- Remove `--tags` and `--match` from `push`
  - If either `--tags` or `--match` is used with `push`, fail the `push` with an error and print a message
    | push cannot be used with either '--tags' or '--match', see [link to forum post] for more on why we removed this broken behavior.
  - Add a deprecation notice to the release note including this change
  - Update the `push --help` text that currently suggests it will filter on `push`
  - Note that `--tags` and `--match` are still valid options for _running_ monitors (but not with `push`)
  - Update the [docs page](https://www.elastic.co/guide/en/observability/current/synthetics-command-reference.html) that `--tags` and `--match` are only relevant when running monitor (not pushing them)
- Tags defined for an **individual monitor**
  - Tags can continue to be defined for an individual monitor via:
    - `monitor.use.tags` of `journey({name:'My Monitor', tags:['foo']}, …`
      - Note that the `journey()` version is not documented or intended for general use (will maybe documented in the future if there's a need)
- Add `monitor.use.tags` support to the `synthetics.config.ts`
  - Precedence order is that this global setting is overridden by any monitor specific setting for `tags`
    - Either `monitor.use.tags` or `journey…` type of definition
  -  i.e. the behaviour is consistent with `locations`
- Tags defined against a monitor in any way are then reflected in the Synthetics UI monitor tags
- Add a sticky forum post for a month or so describing the change
Consolidate the Private Locations configuration for lightweight project monitors
When configuring a private location setting for a lightweight project monitor, it currently uses a `private_locations: []` configuration, which is different to how they are configured for browser monitors (`privateLocations: []`).  This is different, confusing and not documented.

ACs:
* Include an alias configuration to allow the private location to be configured for lightweight project monitors, using `privateLocations: []`
  * To ensure it’s not a breaking change, keep support for `private_locations: []`
* Update the [documentation](https://www.elastic.co/guide/en/observability/current/synthetics-lightweight.html#synthetics-lightweight-projects) to include how to configure the private location via `privateLocations: []`

Pull request:
fix: remove match/tags from push command
+ fix #780
+ fix #787 
+ PR removes the ability to add `--tags`and `--match` to the push command and aborts the command if the command is run with any of the CLI flags.
+ Adds the ability to configure tags for the project based browser and lightweight monitors. Will be overridden by the individual monitor.tags or journey.tags if present on the monitors themselves. 
+ `private_locations` and `privateLocations` are supported in the lightweight monitor config. 
+ `alert.status, alert.tls` was not captured from global config file for browser monitors. Fixed that as well. 


### Testing
- Build the project `npm run build`
- Run the Push command 
```sh
SYNTHETICS_API_KEY=<key> node dist/cli.js push --tags "foo*"
// aborts and logs the error
```
- Test with configuring `monitor.tags` in the synthetics.config file and check if its applied on all monitors in the UI. 


Patch:
diff --git a/src/cli.ts b/src/cli.ts
index 3cef19a..59b720f 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -38,13 +38,7 @@ import { globalSetup } from './loader';
 import { run } from './';
 import { runner } from './core';
 import { SyntheticsLocations } from './dsl/monitor';
-import {
-  push,
-  loadSettings,
-  validateSettings,
-  catchIncorrectSettings,
-  warnIfThrottled,
-} from './push';
+import { push, loadSettings, validatePush, warnIfThrottled } from './push';
 import {
   formatLocations,
   getLocations,
@@ -61,7 +55,7 @@ import { getVersion } from './push/kibana_api';
 /* eslint-disable-next-line @typescript-eslint/no-var-requires */
 const { name, version } = require('../package.json');
 
-const { params, pattern, tags, match, playwrightOpts } = getCommonCommandOpts();
+const { params, pattern, playwrightOpts } = getCommonCommandOpts();
 
 program
   .name(`npx ${name}`)
@@ -71,9 +65,12 @@ program
     'configuration path (default: synthetics.config.js)'
   )
   .addOption(pattern)
-  .addOption(tags)
-  .addOption(match)
   .addOption(params)
+  .option('--tags <name...>', 'run tests with a tag that matches the glob')
+  .option(
+    '--match <name>',
+    'run tests with a name or tags that matches the glob'
+  )
   .addOption(
     new Option('--reporter <value>', `output reporter format`).choices(
       Object.keys(reporters)
@@ -194,8 +191,6 @@ program
   )
   .option('-y, --yes', 'skip all questions and run non-interactively')
   .addOption(pattern)
-  .addOption(tags)
-  .addOption(match)
   .addOption(params)
   .addOption(playwrightOpts)
   .action(async (cmdOpts: PushOptions) => {
@@ -213,8 +208,7 @@ program
         },
         'push'
       ) as PushOptions;
-      validateSettings(options);
-      await catchIncorrectSettings(settings, options);
+      await validatePush(options, settings);
       const monitors = runner.buildMonitors(options);
       if ((options as CliArgs).throttling == null) {
         warnIfThrottled(monitors);
diff --git a/src/common_types.ts b/src/common_types.ts
index 1ac5e1a..c8c14f8 100644
--- a/src/common_types.ts
+++ b/src/common_types.ts
@@ -201,6 +201,7 @@ type BaseArgs = {
   params?: Params;
   screenshots?: ScreenshotOptions;
   dryRun?: boolean;
+  pattern?: string;
   match?: string;
   tags?: Array<string>;
   outfd?: number;
@@ -218,7 +219,6 @@ type BaseArgs = {
 export type CliArgs = BaseArgs & {
   config?: string;
   reporter?: BuiltInReporterName;
-  pattern?: string;
   inline?: boolean;
   require?: Array<string>;
   sandbox?: boolean;
@@ -238,19 +238,12 @@ export type RunOptions = BaseArgs & {
   reporter?: BuiltInReporterName | ReporterInstance;
 };
 
-export type PushOptions = Partial<ProjectSettings> & {
-  auth: string;
-  schedule?: MonitorConfig['schedule'];
-  locations?: MonitorConfig['locations'];
-  privateLocations?: MonitorConfig['privateLocations'];
-  params?: MonitorConfig['params'];
-  kibanaVersion?: number;
-  yes?: boolean;
-  pattern?: string;
-  match?: string;
-  tags?: Array<string>;
-  alert?: AlertConfig;
-};
+export type PushOptions = Partial<ProjectSettings> &
+  Partial<BaseArgs> & {
+    auth: string;
+    kibanaVersion?: number;
+    yes?: boolean;
+  };
 
 export type ProjectSettings = {
   id: string;
@@ -264,6 +257,7 @@ export type PlaywrightOptions = LaunchOptions &
     actionTimeout?: number;
     navigationTimeout?: number;
   };
+
 export type SyntheticsConfig = {
   params?: Params;
   playwrightOptions?: PlaywrightOptions;
diff --git a/src/core/runner.ts b/src/core/runner.ts
index 487b561..6949a66 100644
--- a/src/core/runner.ts
+++ b/src/core/runner.ts
@@ -44,6 +44,7 @@ import {
   RunOptions,
   JourneyResult,
   StepResult,
+  PushOptions,
 } from '../common_types';
 import { PluginManager } from '../plugins';
 import { PerformanceManager } from '../plugins';
@@ -383,7 +384,7 @@ export default class Runner {
     await mkdir(CACHE_PATH, { recursive: true });
   }
 
-  buildMonitors(options: RunOptions) {
+  buildMonitors(options: PushOptions) {
     /**
      * Update the global monitor configuration required for
      * setting defaults
@@ -396,13 +397,12 @@ export default class Runner {
       params: options.params,
       playwrightOptions: options.playwrightOptions,
       screenshot: options.screenshots,
+      tags: options.tags,
+      alert: options.alert,
     });
 
     const monitors: Monitor[] = [];
     for (const journey of this.journeys) {
-      if (!journey.isMatch(options.match, options.tags)) {
-        continue;
-      }
       this.#currentJourney = journey;
       /**
        * Execute dummy callback to get all monitor specific
diff --git a/src/dsl/monitor.ts b/src/dsl/monitor.ts
index 0c664b3..f5c488b 100644
--- a/src/dsl/monitor.ts
+++ b/src/dsl/monitor.ts
@@ -91,7 +91,10 @@ export class Monitor {
   update(globalOpts: MonitorConfig = {}) {
     this.config = merge(globalOpts, this.config, {
       arrayMerge(target, source) {
-        return [...new Set(source)];
+        if (source && source.length > 0) {
+          return [...new Set(source)];
+        }
+        return target;
       },
     });
   }
diff --git a/src/options.ts b/src/options.ts
index afb28ad..58a3994 100644
--- a/src/options.ts
+++ b/src/options.ts
@@ -26,8 +26,8 @@
 import merge from 'deepmerge';
 import { createOption } from 'commander';
 import { readConfig } from './config';
-import type { CliArgs, RunOptions } from './common_types';
-import { THROTTLING_WARNING_MSG, warn } from './helpers';
+import type { CliArgs, PushOptions, RunOptions } from './common_types';
+import { THROTTLING_WARNING_MSG, error, warn } from './helpers';
 
 type Mode = 'run' | 'push';
 
@@ -120,22 +120,33 @@ export function normalizeOptions(
       options.screenshots = cliArgs.screenshots ?? 'on';
       break;
     case 'push':
+      validatePushOptions(options as PushOptions);
       /**
        * Merge the default monitor config from synthetics.config.ts file
        * with the CLI options passed via push command
        */
       const monitor = config.monitor;
-      options.screenshots = options.screenshots ?? monitor?.screenshot;
-      options.schedule = options.schedule ?? monitor?.schedule;
-      options.locations = options.locations ?? monitor?.locations;
-      options.privateLocations =
-        options.privateLocations ?? monitor?.privateLocations;
-      options.alert = options.alert ?? monitor?.alert;
+      for (const key of Object.keys(monitor || {})) {
+        // screenshots require special handling as the flags are different
+        if (key === 'screenshot') {
+          options.screenshots = options.screenshots ?? monitor[key];
+          continue;
+        }
+        options[key] = options[key] ?? monitor[key];
+      }
       break;
   }
   return options;
 }
 
+export function validatePushOptions(opts: PushOptions) {
+  if (opts.tags || opts.match) {
+    throw error(`Aborted. Invalid CLI flags.
+
+Tags and Match are not supported in push command.`);
+  }
+}
+
 /* eslint-disable-next-line @typescript-eslint/no-unused-vars */
 function toObject(value: boolean | Record<string, any>): Record<string, any> {
   const defaulVal = {};
@@ -173,20 +184,10 @@ export function getCommonCommandOpts() {
     '--pattern <pattern>',
     'RegExp pattern to match journey/monitor files that are different from the default (ex: /*.journey.(ts|js)$/)'
   );
-  const tags = createOption(
-    '--tags <name...>',
-    'run/push tests with a tag that matches the glob'
-  );
-  const match = createOption(
-    '--match <name>',
-    'run/push tests with a name or tags that matches the glob'
-  );
 
   return {
     params,
     playwrightOpts,
     pattern,
-    tags,
-    match,
   };
 }
diff --git a/src/push/index.ts b/src/push/index.ts
index 2c6f228..9234496 100644
--- a/src/push/index.ts
+++ b/src/push/index.ts
@@ -174,6 +174,14 @@ ${INSTALLATION_HELP}`);
   }
 }
 
+export async function validatePush(
+  opts: PushOptions,
+  settings: ProjectSettings
+) {
+  validateSettings(opts);
+  await catchIncorrectSettings(settings, opts);
+}
+
 export function validateSettings(opts: PushOptions) {
   const INVALID = 'Aborted. Invalid synthetics project settings.';
 
diff --git a/src/push/monitor.ts b/src/push/monitor.ts
index e36f770..cd18654 100644
--- a/src/push/monitor.ts
+++ b/src/push/monitor.ts
@@ -28,7 +28,7 @@ import { extname, join } from 'path';
 import { LineCounter, parseDocument, YAMLSeq, YAMLMap } from 'yaml';
 import { bold, red } from 'kleur/colors';
 import { Bundler } from './bundler';
-import { SYNTHETICS_PATH, totalist, indent, warn, isMatch } from '../helpers';
+import { SYNTHETICS_PATH, totalist, indent, warn } from '../helpers';
 import { LocationsMap } from '../locations/public-locations';
 import {
   AlertConfig,
@@ -203,10 +203,6 @@ export async function createLightweightMonitors(
         continue;
       }
       const config = monNode.toJSON();
-      // Filter based on matched tags and name
-      if (!isMatch(config.tags, config.name, options.tags, options.match)) {
-        continue;
-      }
       const { line, col } = lineCounter.linePos(monNode.srcToken.offset);
       try {
         const mon = buildMonitorFromYaml(config, options);
@@ -237,11 +233,14 @@ export function buildMonitorFromYaml(
   }
   const schedule = config.schedule && parseSchedule(String(config.schedule));
   const privateLocations =
-    config['private_locations'] || options.privateLocations;
+    config['private_locations'] ||
+    config.privateLocations ||
+    options.privateLocations;
   delete config['private_locations'];
   const alertConfig = parseAlertConfig(config, options.alert);
   const mon = new Monitor({
     locations: options.locations,
+    tags: options.tags,
     ...config,
     privateLocations,
     schedule: schedule || options.schedule,
@@ -260,42 +259,54 @@ export function buildMonitorFromYaml(
 
 export const parseAlertConfig = (
   config: MonitorConfig,
-  globalAlertConfig?: AlertConfig
+  gConfig?: AlertConfig
 ) => {
-  const alertConfig: AlertConfig = {};
-
-  if (config['alert.status.enabled'] !== undefined) {
-    const value = config['alert.status.enabled'];
-    delete config['alert.status.enabled'];
-    alertConfig['status'] = {
-      enabled: value,
-    };
+  // If the user has provided a global alert config, merge it with the monitor alert config
+  const status = getAlertKeyValue('status', config, gConfig);
+  const tls = getAlertKeyValue('tls', config, gConfig);
+  const result = {};
+  if (status) {
+    result['status'] = status;
+  }
+  if (tls) {
+    result['tls'] = tls;
   }
-  if (config['alert.tls.enabled'] !== undefined) {
-    const value = config['alert.tls.enabled'];
-    delete config['alert.tls.enabled'];
-    alertConfig['tls'] = {
-      enabled: value,
+  return Object.keys(result).length > 0 ? result : undefined;
+};
+
+export function getAlertKeyValue(
+  key: 'status' | 'tls',
+  config: MonitorConfig,
+  alertConfig?: AlertConfig
+): { enabled: boolean } {
+  const value = config.alert;
+  if (value?.[key]?.enabled !== undefined) {
+    return {
+      enabled: value[key].enabled,
     };
   }
-  if (config?.alert?.status?.enabled !== undefined) {
-    alertConfig.status = {
-      enabled: config.alert.status.enabled,
+
+  if (value?.[`${key}.enabled`] !== undefined) {
+    const val = value?.[`${key}.enabled`];
+    delete value?.[`${key}.enabled`];
+    if (Object.keys(value).length === 0) {
+      delete config.alert;
+    }
+    return {
+      enabled: val,
     };
   }
-  if (config?.alert?.tls?.enabled !== undefined) {
-    alertConfig.tls = {
-      enabled: config.alert.tls.enabled,
+  const rootKey = `alert.${key}.enabled`;
+  if (config[rootKey] !== undefined) {
+    const enabled = config[rootKey];
+    delete config[rootKey];
+    return {
+      enabled,
     };
   }
 
-  // If the user has provided a global alert config, merge it with the monitor alert config
-  const result = {
-    ...(globalAlertConfig ?? {}),
-    ...alertConfig,
-  };
-  return Object.keys(result).length > 0 ? result : undefined;
-};
+  return alertConfig?.[key];
+}
 
 export function parseSchedule(schedule: string) {
   const EVERY_SYNTAX = '@every';


Test patch:
diff --git a/__tests__/core/runner.test.ts b/__tests__/core/runner.test.ts
index d37cd6b..363c6b8 100644
--- a/__tests__/core/runner.test.ts
+++ b/__tests__/core/runner.test.ts
@@ -707,106 +707,90 @@ describe('runner', () => {
     expect(collectOrder).toEqual(realEventsOrder);
   });
 
-  it('runner - build monitors with local config', async () => {
-    const j1 = new Journey({ name: 'j1' }, noop);
-    const j2 = new Journey({ name: 'j2' }, noop);
-    j1.updateMonitor({
-      id: 'test-j1',
-      schedule: 3,
-      locations: ['united_kingdom'],
-    });
-    j2.updateMonitor({ throttling: { latency: 1000 }, schedule: 1 });
-    runner.addJourney(j1);
-    runner.addJourney(j2);
-
-    const monitors = runner.buildMonitors({
-      throttling: DEFAULT_THROTTLING_OPTIONS,
-    });
-    expect(monitors.length).toBe(2);
-    expect(monitors[0].config).toEqual({
-      id: 'test-j1',
-      name: 'j1',
-      type: 'browser',
-      tags: [],
-      locations: ['united_kingdom'],
-      privaateLocations: undefined,
-      schedule: 3,
-      params: undefined,
-      playwrightOptions: undefined,
-      throttling: { download: 5, latency: 20, upload: 3 },
-    });
-    expect(monitors[1].config).toMatchObject({
-      throttling: { latency: 1000 },
-    });
-  });
-
-  it('runner - build monitors with global config', async () => {
-    runner.updateMonitor({
-      schedule: 5,
-      locations: ['us_east'],
-      privateLocations: ['germany'],
-      throttling: { download: 100, upload: 50 },
-      params: { env: 'test' },
-      playwrightOptions: { ignoreHTTPSErrors: true },
+  describe('build monitors', () => {
+    const options = { auth: 'foo', throttling: DEFAULT_THROTTLING_OPTIONS };
+    it('runner - build monitors with local config', async () => {
+      const j1 = new Journey({ name: 'j1', tags: ['j1', 'j2'] }, noop);
+      const j2 = new Journey({ name: 'j2' }, noop);
+      j1.updateMonitor({
+        id: 'test-j1',
+        schedule: 3,
+        tags: ['l1', 'l2'],
+        locations: ['united_kingdom'],
+      });
+      j2.updateMonitor({
+        throttling: { latency: 1000 },
+        schedule: 1,
+        alert: { status: { enabled: false } },
+      });
+      runner.addJourney(j1);
+      runner.addJourney(j2);
+
+      const monitors = runner.buildMonitors(options);
+      expect(monitors.length).toBe(2);
+      expect(monitors[0].config).toEqual({
+        id: 'test-j1',
+        name: 'j1',
+        type: 'browser',
+        tags: ['l1', 'l2'],
+        locations: ['united_kingdom'],
+        schedule: 3,
+        throttling: { download: 5, latency: 20, upload: 3 },
+      });
+      expect(monitors[1].config).toMatchObject({
+        throttling: { latency: 1000 },
+        schedule: 1,
+        alert: { status: { enabled: false } },
+      });
     });
 
-    const j1 = new Journey({ name: 'j1', tags: ['foo*'] }, noop);
-    const j2 = new Journey({ name: 'j2' }, noop);
-    j1.updateMonitor({
-      id: 'test-j1',
-      schedule: 3,
-      locations: ['united_kingdom'],
-      privateLocations: ['spain'],
-    });
-    j2.updateMonitor({ throttling: { latency: 1000 } });
-    runner.addJourney(j1);
-    runner.addJourney(j2);
+    it('runner - build monitors with global config', async () => {
+      runner.updateMonitor({
+        schedule: 5,
+        locations: ['us_east'],
+        privateLocations: ['germany'],
+        throttling: { download: 100, upload: 50 },
+        params: { env: 'test' },
+        tags: ['g1', 'g2'],
+        alert: { tls: { enabled: true } },
+        playwrightOptions: { ignoreHTTPSErrors: true },
+      });
 
-    const monitors = runner.buildMonitors({
-      throttling: DEFAULT_THROTTLING_OPTIONS,
-    });
-    expect(monitors.length).toBe(2);
-    expect(monitors[0].config).toEqual({
-      id: 'test-j1',
-      name: 'j1',
-      type: 'browser',
-      tags: ['foo*'],
-      locations: ['united_kingdom'],
-      privateLocations: ['spain'],
-      schedule: 3,
-      params: { env: 'test' },
-      playwrightOptions: { ignoreHTTPSErrors: true },
-      throttling: { download: 100, latency: 20, upload: 50 },
-    });
-    expect(monitors[1].config).toMatchObject({
-      locations: ['us_east'],
-      privateLocations: ['germany'],
-      schedule: 5,
-      throttling: { latency: 1000 },
+      const j1 = new Journey({ name: 'j1', tags: ['foo*'] }, noop);
+      const j2 = new Journey({ name: 'j2' }, noop);
+      j1.updateMonitor({
+        id: 'test-j1',
+        schedule: 3,
+        locations: ['united_kingdom'],
+        privateLocations: ['spain'],
+      });
+      j2.updateMonitor({ throttling: { latency: 1000 } });
+      runner.addJourney(j1);
+      runner.addJourney(j2);
+
+      const monitors = runner.buildMonitors(options);
+      expect(monitors.length).toBe(2);
+      expect(monitors[0].config).toEqual({
+        id: 'test-j1',
+        name: 'j1',
+        type: 'browser',
+        tags: ['foo*'],
+        locations: ['united_kingdom'],
+        privateLocations: ['spain'],
+        schedule: 3,
+        params: { env: 'test' },
+        playwrightOptions: { ignoreHTTPSErrors: true },
+        throttling: { download: 100, latency: 20, upload: 50 },
+        alert: { tls: { enabled: true } },
+      });
+      expect(monitors[1].config).toMatchObject({
+        locations: ['us_east'],
+        privateLocations: ['germany'],
+        schedule: 5,
+        tags: ['g1', 'g2'],
+        throttling: { latency: 1000 },
+        alert: { tls: { enabled: true } },
+      });
     });
   });
-
-  it('runner - build monitors filtered through "match"', async () => {
-    const j1 = new Journey({ name: 'j1' }, noop);
-    const j2 = new Journey({ name: 'j2' }, noop);
-    runner.addJourney(j1);
-    runner.addJourney(j2);
-
-    const monitors = runner.buildMonitors({ match: 'j1', schedule: 1 });
-    expect(monitors.length).toBe(1);
-    expect(monitors[0].config.name).toBe('j1');
-  });
-
-  it('runner - build monitors filtered through "tags"', async () => {
-    const j1 = new Journey({ name: 'j1', tags: ['first'] }, noop);
-    const j2 = new Journey({ name: 'j2', tags: ['second'] }, noop);
-    const j3 = new Journey({ name: 'j3' }, noop);
-    runner.addJourney(j1);
-    runner.addJourney(j2);
-    runner.addJourney(j3);
-
-    const monitors = runner.buildMonitors({ tags: ['first'], schedule: 1 });
-    expect(monitors.length).toBe(1);
-    expect(monitors[0].config.name).toBe('j1');
-  });
 });
diff --git a/__tests__/push/__snapshots__/index.test.ts.snap b/__tests__/push/__snapshots__/index.test.ts.snap
index 1ec22f7..c4d8e0b 100644
--- a/__tests__/push/__snapshots__/index.test.ts.snap
+++ b/__tests__/push/__snapshots__/index.test.ts.snap
@@ -1,5 +1,12 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`Push abort on tags and match 1`] = `
+"Aborted. Invalid CLI flags.
+
+Tags and Match are not supported in push command.
+"
+`;
+
 exports[`Push error on empty project id 1`] = `
 "Aborted. Invalid synthetics project settings.
 
diff --git a/__tests__/push/index.test.ts b/__tests__/push/index.test.ts
index a7a52f3..d8a3b12 100644
--- a/__tests__/push/index.test.ts
+++ b/__tests__/push/index.test.ts
@@ -119,6 +119,12 @@ describe('Push', () => {
     expect(output).toContain('Push command Aborted');
   });
 
+  it('abort on tags and match', async () => {
+    await fakeProjectSetup({}, {});
+    const output = await runPush([...DEFAULT_ARGS, '--tags', 'foo:*']);
+    expect(output).toMatchSnapshot();
+  });
+
   it('error on invalid schedule in monitor DSL', async () => {
     await fakeProjectSetup(
       { id: 'test-project', space: 'dummy', url: 'http://localhost:8080' },
diff --git a/__tests__/push/monitor.test.ts b/__tests__/push/monitor.test.ts
index c5bfb01..25a86bf 100644
--- a/__tests__/push/monitor.test.ts
+++ b/__tests__/push/monitor.test.ts
@@ -132,58 +132,6 @@ describe('Monitors', () => {
     expect(parseSchedule('@every 1d')).toBe(240);
   });
 
-  it('parse alert config option', async () => {
-    expect(parseAlertConfig({})).toBe(undefined);
-    expect(parseAlertConfig({ 'alert.status.enabled': true } as any)).toEqual({
-      status: { enabled: true },
-    });
-    expect(parseAlertConfig({ alert: { status: { enabled: true } } })).toEqual({
-      status: { enabled: true },
-    });
-
-    expect(
-      parseAlertConfig(
-        { alert: { status: { enabled: true } } },
-        {
-          status: { enabled: false },
-          tls: { enabled: true },
-        }
-      )
-    ).toEqual({
-      status: { enabled: true },
-      tls: {
-        enabled: true,
-      },
-    });
-  });
-
-  it('parse tls alert config option', async () => {
-    expect(parseAlertConfig({})).toBe(undefined);
-    expect(
-      parseAlertConfig({
-        'alert.status.enabled': true,
-        'alert.tls.enabled': true,
-      } as any)
-    ).toEqual({
-      status: { enabled: true },
-      tls: { enabled: true },
-    });
-    expect(parseAlertConfig({ 'alert.tls.enabled': true } as any)).toEqual({
-      tls: { enabled: true },
-    });
-    expect(
-      parseAlertConfig(
-        { alert: { tls: { enabled: true } } },
-        {
-          status: { enabled: false },
-        }
-      )
-    ).toEqual({
-      tls: { enabled: true },
-      status: { enabled: false },
-    });
-  });
-
   describe('Lightweight monitors', () => {
     const PROJECT_DIR = generateTempPath();
     const HB_SOURCE = join(PROJECT_DIR, 'heartbeat.yml');
@@ -285,78 +233,62 @@ heartbeat.monitors:
       expect(monitors.length).toBe(0);
     });
 
-    it('use schedule from config', async () => {
+    it('prefer local monitor config', async () => {
       await writeHBFile(`
 heartbeat.monitors:
 - type: icmp
-  id: foo
-  name: without schedule
-      `);
-      const [monitor] = await createLightweightMonitors(PROJECT_DIR, {
-        schedule: 10,
-      } as any);
-      expect(monitor.config).toMatchObject({
-        id: 'foo',
-        name: 'without schedule',
-        type: 'icmp',
-        schedule: 10,
-      });
-    });
-
-    it('use params from config', async () => {
-      await writeHBFile(`
-heartbeat.monitors:
-- type: icmp
-  id: foo
-  name: without schedule
+  schedule: @every 5m
+  id: "test-icmp"
+  name: "test-icmp"
+  privateLocations:
+    - baz
+  tags:
+    - ltag1
+    - ltag2
       `);
-      const [monitor1] = await createLightweightMonitors(
-        PROJECT_DIR,
-        {} as PushOptions
-      );
-      expect(monitor1.config).toMatchObject({
-        id: 'foo',
-        name: 'without schedule',
-        type: 'icmp',
-      });
 
-      const [monitor2] = await createLightweightMonitors(PROJECT_DIR, {
+      const [mon] = await createLightweightMonitors(PROJECT_DIR, {
         params: { foo: 'bar' },
         kibanaVersion: '8.8.0',
+        locations: ['australia_east'],
+        tags: ['gtag1', 'gtag2'],
+        privateLocations: ['gbaz'],
+        schedule: 10,
       } as any);
-      expect(monitor2.config).toEqual({
-        id: 'foo',
-        name: 'without schedule',
+
+      expect(mon.config).toEqual({
+        id: 'test-icmp',
+        name: 'test-icmp',
+        locations: ['australia_east'],
+        privateLocations: ['baz'],
         type: 'icmp',
         params: { foo: 'bar' },
+        schedule: 5,
+        tags: ['ltag1', 'ltag2'],
       });
     });
 
-    it('parses monitor config correctly', async () => {
+    it('fallback to global monitor config', async () => {
       await writeHBFile(`
 heartbeat.monitors:
 - type: icmp
-  schedule: @every 5m
-  id: "foo"
-  name: "with-loc"
-  private_locations:
-    - baz
+  id: "test-icmp"
+  name: "test-icmp"
       `);
-      const [monitor] = await createLightweightMonitors(PROJECT_DIR, {
-        locations: ['australia_east'],
-      } as any);
-      expect(monitor.config).toEqual({
-        id: 'foo',
-        name: 'with-loc',
+
+      const [mon] = await createLightweightMonitors(PROJECT_DIR, {
+        tags: ['gtag1', 'gtag2'],
+        privateLocations: ['gbaz'],
+        schedule: 10,
+      } as PushOptions);
+
+      expect(mon.config).toEqual({
+        id: 'test-icmp',
+        name: 'test-icmp',
+        privateLocations: ['gbaz'],
         type: 'icmp',
-        locations: ['australia_east'],
-        privateLocations: ['baz'],
-        schedule: 5,
-      });
-      expect(monitor.source).toEqual({
-        file: HB_SOURCE,
-        column: 3,
-        line: 3,
+        schedule: 10,
+        tags: ['gtag1', 'gtag2'],
       });
     });
 
@@ -406,4 +338,102 @@ heartbeat.monitors:
       });
     });
   });
+
+  describe('parseAlertConfig', () => {
+    it('parse alert config option', async () => {
+      expect(parseAlertConfig({})).toBe(undefined);
+      expect(parseAlertConfig({ 'alert.status.enabled': true } as any)).toEqual(
+        {
+          status: { enabled: true },
+        }
+      );
+      expect(
+        parseAlertConfig({ alert: { status: { enabled: true } } })
+      ).toEqual({
+        status: { enabled: true },
+      });
+    });
+
+    it('parse alert config option when global config is also provided', async () => {
+      expect(
+        parseAlertConfig(
+          { alert: { 'status.enabled': false, 'tls.enabled': true } } as any,
+          {
+            status: { enabled: false },
+            tls: { enabled: false },
+          }
+        )
+      ).toEqual({
+        status: { enabled: false },
+        tls: {
+          enabled: true,
+        },
+      });
+      expect(
+        parseAlertConfig(
+          { alert: { status: { enabled: true } } },
+          {
+            status: { enabled: false },
+            tls: { enabled: true },
+          }
+        )
+      ).toEqual({
+        status: { enabled: true },
+        tls: {
+          enabled: true,
+        },
+      });
+    });
+
+    it('parse tls alert config option', async () => {
+      expect(parseAlertConfig({})).toBe(undefined);
+      expect(
+        parseAlertConfig({
+          'alert.status.enabled': true,
+          'alert.tls.enabled': true,
+        } as any)
+      ).toEqual({
+        status: { enabled: true },
+        tls: { enabled: true },
+      });
+      expect(parseAlertConfig({ 'alert.tls.enabled': true } as any)).toEqual({
+        tls: { enabled: true },
+      });
+      expect(
+        parseAlertConfig(
+          { alert: { tls: { enabled: true } } },
+          {
+            status: { enabled: false },
+          }
+        )
+      ).toEqual({
+        tls: { enabled: true },
+        status: { enabled: false },
+      });
+    });
+
+    it('deletes parsed keys from config', async () => {
+      let config: any = {
+        'alert.status.enabled': true,
+        'alert.tls.enabled': true,
+      };
+      expect(parseAlertConfig(config)).toEqual({
+        status: { enabled: true },
+        tls: { enabled: true },
+      });
+      expect(config).toEqual({});
+
+      config = {
+        alert: {
+          'status.enabled': true,
+          'tls.enabled': true,
+        },
+      };
+      expect(parseAlertConfig(config)).toEqual({
+        status: { enabled: true },
+        tls: { enabled: true },
+      });
+      expect(config).toEqual({});
+    });
+  });
 });