adapty 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -293,6 +293,21 @@ When several agents are found, the command asks which to install into; with no o
293
293
  `npx skills add adaptyteam/adapty-skills --all --global`, which covers the other agents. Restart an agent to
294
294
  load the skills. Exit **1** means an install failed.
295
295
 
296
+ ### Feedback
297
+
298
+ A short report to Adapty on how a task with the CLI or an Adapty skill went. Leave out secrets and personal
299
+ data. There is no `--app`: put app, flow or other ids into the message when they matter:
300
+
301
+ ```sh
302
+ adapty feedback --sentiment positive --message "Imported the paywall from Canva in one pass"
303
+ adapty feedback --sentiment negative --message "flows config update rejected a valid button twice" \
304
+ --feature flow-generator --context "editing the paywall screen"
305
+ ```
306
+
307
+ `--sentiment` is `positive`, `neutral` or `negative`. `--feature` takes letters, digits, `.`, `_` and `-`;
308
+ Adapty records `unknown` when it is left out. The CLI won't send a report containing something that looks like
309
+ an API key, a bearer token, a JWT or an email address; it exits 2 and names the flag to fix.
310
+
296
311
  ### Apple Search Ads
297
312
 
298
313
  Apple Search Ads commands live under `adapty asa` and talk to the ASA service rather than the Developer
package/bin/dev.js CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env -S node --loader ts-node/esm --disable-warning=ExperimentalWarning
2
2
 
3
+ import net from 'node:net';
4
+
3
5
  import { execute } from '@oclif/core';
4
6
 
7
+ // Node abandons every address but the last after 250ms. On a slow link the working address is
8
+ // dropped and the last one, e.g. IPv6 with no route, fails. Never lower a user-set value.
9
+ net.setDefaultAutoSelectFamilyAttemptTimeout(Math.max(net.getDefaultAutoSelectFamilyAttemptTimeout(), 1000));
10
+
5
11
  await execute({ development: true, dir: import.meta.url });
package/bin/run.js CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import net from 'node:net';
4
+
3
5
  import { execute } from '@oclif/core';
4
6
 
7
+ // Node abandons every address but the last after 250ms. On a slow link the working address is
8
+ // dropped and the last one, e.g. IPv6 with no route, fails. Never lower a user-set value.
9
+ net.setDefaultAutoSelectFamilyAttemptTimeout(Math.max(net.getDefaultAutoSelectFamilyAttemptTimeout(), 1000));
10
+
5
11
  await execute({ dir: import.meta.url });
@@ -0,0 +1,16 @@
1
+ import { AdaptyCommand } from '../base/adapty/index.js';
2
+ type Result = {
3
+ status: 'sent';
4
+ };
5
+ export default class Feedback extends AdaptyCommand {
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ sentiment: import("@oclif/core/interfaces").OptionFlag<"positive" | "neutral" | "negative", import("@oclif/core/interfaces").CustomOptions>;
10
+ message: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ feature: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ context: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ };
14
+ run(): Promise<Result>;
15
+ }
16
+ export {};
@@ -0,0 +1,36 @@
1
+ import { Flags } from '@oclif/core';
2
+ import { validateSendFeedback } from '../../sdk/adapty/feedback/index.js';
3
+ import { sentiments } from '../../sdk/adapty/index.js';
4
+ import { assertValid } from '../../sdk/core/validation.js';
5
+ import { AdaptyCommand } from '../base/adapty/index.js';
6
+ export default class Feedback extends AdaptyCommand {
7
+ static description = [
8
+ 'Send a short report on how a task with the Adapty CLI or an Adapty skill went.',
9
+ 'Meant for AI agents: say what worked, what failed or confused you. Put app, flow and other ids into the message if they matter.',
10
+ 'Leave out secrets and personal data.',
11
+ ].join('\n');
12
+ static examples = [
13
+ '<%= config.bin %> feedback --sentiment negative --feature flow-generator --message "flows config update rejected a valid button component twice"',
14
+ '<%= config.bin %> feedback --sentiment positive --message "Imported the paywall from Canva in one pass" --context "Building an onboarding flow from a Canva file"',
15
+ ];
16
+ static flags = {
17
+ sentiment: Flags.option({ description: 'How the task went', options: sentiments, required: true })(),
18
+ message: Flags.string({ description: 'The report: a few sentences on what happened', required: true }),
19
+ feature: Flags.string({ description: 'Feature or skill the report comes from; Adapty records "unknown" when omitted' }),
20
+ context: Flags.string({ description: 'What you were doing at that moment' }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(Feedback);
24
+ const input = {
25
+ context: flags.context,
26
+ feature: flags.feature,
27
+ message: flags.message,
28
+ sentiment: flags.sentiment,
29
+ };
30
+ assertValid(validateSendFeedback(input));
31
+ await this.adapty.feedback.send(input);
32
+ const result = { status: 'sent' };
33
+ this.render(result, () => 'Feedback sent. Thank you!');
34
+ return result;
35
+ }
36
+ }
@@ -0,0 +1 @@
1
+ export { default } from '../cli/commands/feedback.js';
@@ -0,0 +1 @@
1
+ export { default } from '../cli/commands/feedback.js';
@@ -0,0 +1,4 @@
1
+ export { feedback } from './resource.js';
2
+ export { sentiments, validateSendFeedback } from './send.js';
3
+ export type { FeedbackApi } from './resource.js';
4
+ export type { SendFeedbackInput, Sentiment } from './send.js';
@@ -0,0 +1,2 @@
1
+ export { feedback } from './resource.js';
2
+ export { sentiments, validateSendFeedback } from './send.js';
@@ -0,0 +1,6 @@
1
+ import type { SendFeedbackInput } from './send.js';
2
+ import type { Http } from '../../core/http/index.js';
3
+ export declare const feedback: (http: Http) => {
4
+ send: (input: SendFeedbackInput) => Promise<void>;
5
+ };
6
+ export type FeedbackApi = ReturnType<typeof feedback>;
@@ -0,0 +1,8 @@
1
+ import { assertValid } from '../../core/validation.js';
2
+ import { toSendRequest, validateSendFeedback } from './send.js';
3
+ export const feedback = (http) => ({
4
+ send: async (input) => {
5
+ assertValid(validateSendFeedback(input));
6
+ await http.post('/feedback', toSendRequest(input));
7
+ },
8
+ });
@@ -0,0 +1,18 @@
1
+ import type { Issue } from '../../core/errors.js';
2
+ export declare const sentiments: readonly ["positive", "neutral", "negative"];
3
+ export type Sentiment = (typeof sentiments)[number];
4
+ export type SendFeedbackInput = {
5
+ context?: string | undefined;
6
+ feature?: string | undefined;
7
+ message: string;
8
+ sentiment: Sentiment;
9
+ };
10
+ type SendFeedbackRequest = {
11
+ context?: string;
12
+ feature?: string;
13
+ message: string;
14
+ sentiment: Sentiment;
15
+ };
16
+ export declare const validateSendFeedback: (input: SendFeedbackInput) => Issue[];
17
+ export declare const toSendRequest: (input: SendFeedbackInput) => SendFeedbackRequest;
18
+ export {};
@@ -0,0 +1,36 @@
1
+ export const sentiments = ['positive', 'neutral', 'negative'];
2
+ // A coarse first pass: the server scrubs reports more thoroughly.
3
+ const secrets = [
4
+ { kind: 'an API key or token', pattern: /(?<![a-z\d])(?:dev|secret|sk|pk|rk)_(?:live|test)_(?=[a-z\d]{0,64}\d)[a-z\d]{6,}/i },
5
+ { kind: 'a bearer token', pattern: /\bbearer\s+(?=[\w.~+-]{0,64}\d)[\w.~+-]{16,}/i },
6
+ { kind: 'a JWT', pattern: /(?<![\w-])eyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]+/ },
7
+ // Lookbehinds and bounded repeats keep matching linear on a long pasted log.
8
+ { kind: 'an email address', pattern: /(?<![\w.%+-])(?!git@)[\w.%+-]{1,64}@[a-z][a-z\d-]*(?:\.[a-z\d-]+)*\.[a-z]{2,}\b/i },
9
+ ];
10
+ const checkSecrets = (issues, path, value) => {
11
+ if (value === undefined) {
12
+ return;
13
+ }
14
+ for (const { kind, pattern } of secrets) {
15
+ if (pattern.test(value)) {
16
+ issues.push({ message: `looks like it contains ${kind}; remove it and send again`, path });
17
+ }
18
+ }
19
+ };
20
+ export const validateSendFeedback = (input) => {
21
+ const issues = [];
22
+ checkSecrets(issues, 'message', input.message);
23
+ checkSecrets(issues, 'context', input.context);
24
+ checkSecrets(issues, 'feature', input.feature);
25
+ return issues;
26
+ };
27
+ export const toSendRequest = (input) => {
28
+ const body = { message: input.message, sentiment: input.sentiment };
29
+ if (input.feature !== undefined) {
30
+ body.feature = input.feature;
31
+ }
32
+ if (input.context !== undefined) {
33
+ body.context = input.context;
34
+ }
35
+ return body;
36
+ };
@@ -1,14 +1,17 @@
1
1
  import type { AccessLevelsApi } from './access-levels.js';
2
2
  import type { AppsApi } from './apps/index.js';
3
3
  import type { AuthApi } from './auth/index.js';
4
+ import type { FeedbackApi } from './feedback/index.js';
4
5
  import type { MigrationApi } from './migrations/index.js';
5
6
  import type { Clock } from '../core/clock.js';
6
7
  import type { RetryAttempt } from '../core/http/index.js';
7
8
  export { platforms } from './apps/index.js';
8
9
  export { developerErrorParser } from './errors.js';
10
+ export { sentiments } from './feedback/index.js';
9
11
  export type { AccessLevel, AccessLevelList, AccessLevelsApi } from './access-levels.js';
10
12
  export type { AppDetail, AppsApi, AppSummary, CreateAppInput, UpdateAppInput } from './apps/index.js';
11
13
  export type { AuthApi, AuthUser, IssuedToken } from './auth/index.js';
14
+ export type { FeedbackApi, SendFeedbackInput, Sentiment } from './feedback/index.js';
12
15
  export type { Action, ActionKind, AvailableFlow, CreateMigrationInput, Envelope, Issue, JsonSchema, Migration, MigrationApi, MigrationList, MigrationState, Progress, ResourceRef, Step, StepStatus, WaitOptions, WizardError, } from './migrations/index.js';
13
16
  export type { PageParams, Paginated, Pagination } from './pagination.js';
14
17
  /** Exported because the adapter compares the resolved URL with it and warns when they differ. */
@@ -35,6 +38,7 @@ export type Adapty = {
35
38
  accessLevels: AccessLevelsApi;
36
39
  apps: AppsApi;
37
40
  auth: AuthApi;
41
+ feedback: FeedbackApi;
38
42
  migrations: MigrationApi;
39
43
  };
40
44
  /** The assembly point of the developer API: one transport, resources on top of it. */
@@ -4,9 +4,11 @@ import { accessLevels } from './access-levels.js';
4
4
  import { apps } from './apps/index.js';
5
5
  import { auth } from './auth/index.js';
6
6
  import { developerErrorParser } from './errors.js';
7
+ import { feedback } from './feedback/index.js';
7
8
  import { migrations } from './migrations/index.js';
8
9
  export { platforms } from './apps/index.js';
9
10
  export { developerErrorParser } from './errors.js';
11
+ export { sentiments } from './feedback/index.js';
10
12
  /** Exported because the adapter compares the resolved URL with it and warns when they differ. */
11
13
  export const DEFAULT_ADAPTY_API_URL = 'https://api-admin.adapty.io/api/v1/developer';
12
14
  /**
@@ -46,6 +48,7 @@ export const createAdapty = (options = {}) => {
46
48
  accessLevels: accessLevels(http),
47
49
  apps: apps(http),
48
50
  auth: auth(http),
51
+ feedback: feedback(http),
49
52
  // The clock is a dependency here too, not only in the transport: `waitFor` sleeps
50
53
  // between polls, and a test must be able to do that instantly.
51
54
  migrations: migrations(wizard, options.clock ?? systemClock),