• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2021 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// This file should not import anything else. Since the flags will be used from
16// ~everywhere and the are "statically" initialized (i.e. files construct Flags
17// at import time) if this file starts importing anything we will quickly run
18// into issues with initialization order which will be a pain.
19import {z} from 'zod';
20import {Flag, FlagSettings, OverrideState} from '../public/feature_flag';
21
22export interface FlagStore {
23  load(): object;
24  save(o: object): void;
25}
26
27// Stored state for a number of flags.
28interface FlagOverrides {
29  [id: string]: OverrideState;
30}
31
32class Flags {
33  private store: FlagStore;
34  private flags: Map<string, FlagImpl>;
35  private overrides: FlagOverrides;
36
37  constructor(store: FlagStore) {
38    this.store = store;
39    this.flags = new Map();
40    this.overrides = {};
41    this.load();
42  }
43
44  register(settings: FlagSettings): Flag {
45    const id = settings.id;
46    if (this.flags.has(id)) {
47      throw new Error(`Flag with id "${id}" is already registered.`);
48    }
49
50    const saved = this.overrides[id];
51    const state = saved === undefined ? OverrideState.DEFAULT : saved;
52    const flag = new FlagImpl(this, state, settings);
53    this.flags.set(id, flag);
54    return flag;
55  }
56
57  allFlags(): Flag[] {
58    const includeDevFlags = ['127.0.0.1', '::1', 'localhost'].includes(
59      window.location.hostname,
60    );
61
62    let flags = [...this.flags.values()];
63    flags = flags.filter((flag) => includeDevFlags || !flag.devOnly);
64    flags.sort((a, b) => a.name.localeCompare(b.name));
65    return flags;
66  }
67
68  resetAll() {
69    for (const flag of this.flags.values()) {
70      flag.state = OverrideState.DEFAULT;
71    }
72    this.save();
73  }
74
75  load(): void {
76    const o = this.store.load();
77
78    // Check if the given object is a valid FlagOverrides.
79    // This is necessary since someone could modify the persisted flags
80    // behind our backs.
81    const flagsSchema = z.record(
82      z.string(),
83      z.union([z.literal(OverrideState.TRUE), z.literal(OverrideState.FALSE)]),
84    );
85    const {success, data} = flagsSchema.safeParse(o);
86    if (success) {
87      this.overrides = data;
88    }
89  }
90
91  save(): void {
92    for (const flag of this.flags.values()) {
93      if (flag.isOverridden()) {
94        this.overrides[flag.id] = flag.state;
95      } else {
96        delete this.overrides[flag.id];
97      }
98    }
99
100    this.store.save(this.overrides);
101  }
102}
103
104class FlagImpl implements Flag {
105  registry: Flags;
106  state: OverrideState;
107
108  readonly id: string;
109  readonly name: string;
110  readonly description: string;
111  readonly defaultValue: boolean;
112  readonly devOnly: boolean;
113
114  constructor(registry: Flags, state: OverrideState, settings: FlagSettings) {
115    this.registry = registry;
116    this.id = settings.id;
117    this.state = state;
118    this.description = settings.description;
119    this.defaultValue = settings.defaultValue;
120    this.name = settings.name ?? settings.id;
121    this.devOnly = settings.devOnly || false;
122  }
123
124  get(): boolean {
125    switch (this.state) {
126      case OverrideState.TRUE:
127        return true;
128      case OverrideState.FALSE:
129        return false;
130      case OverrideState.DEFAULT:
131      default:
132        return this.defaultValue;
133    }
134  }
135
136  set(value: boolean): void {
137    const next = value ? OverrideState.TRUE : OverrideState.FALSE;
138    if (this.state === next) {
139      return;
140    }
141    this.state = next;
142    this.registry.save();
143  }
144
145  overriddenState(): OverrideState {
146    return this.state;
147  }
148
149  reset() {
150    this.state = OverrideState.DEFAULT;
151    this.registry.save();
152  }
153
154  isOverridden(): boolean {
155    return this.state !== OverrideState.DEFAULT;
156  }
157}
158
159class LocalStorageStore implements FlagStore {
160  static KEY = 'perfettoFeatureFlags';
161
162  load(): object {
163    const s = localStorage.getItem(LocalStorageStore.KEY);
164    let parsed: object;
165    try {
166      parsed = JSON.parse(s ?? '{}');
167    } catch (e) {
168      return {};
169    }
170    if (typeof parsed !== 'object' || parsed === null) {
171      return {};
172    }
173    return parsed;
174  }
175
176  save(o: object): void {
177    const s = JSON.stringify(o);
178    localStorage.setItem(LocalStorageStore.KEY, s);
179  }
180}
181
182export const FlagsForTesting = Flags;
183export const featureFlags = new Flags(new LocalStorageStore());
184