Skip to content

Automated application execution via formplayer#34487

Merged
AmitPhulera merged 34 commits into
masterfrom
sk/app-execution
Apr 24, 2024
Merged

Automated application execution via formplayer#34487
AmitPhulera merged 34 commits into
masterfrom
sk/app-execution

Conversation

@snopoke

@snopoke snopoke commented Apr 23, 2024

Copy link
Copy Markdown
Contributor

Technical Summary

This is a new admin feature which allows configuration of application workflows which will be executed on a schedule e.g. navigate through an app and complete a form.

The purpose of these is to generate 'canary' formplayer traffic for the purposes of monitoring and metrics.

It is expected that a small number of these will be configured per environment using a standardised application. There may also be additional configurations for monitoring enterprise apps.

image

image

image

Safety Assurance

Safety story

This is predominantly new functionality. There are some changes to existing code, notably jsonattrs which is used by repeaters. These changes are tested independently.

Automated test coverage

Some test coverage is added in this PR.

QA Plan

None

Migrations

  • The migrations in this code can be safely applied first independently of the code

Rollback instructions

  • This PR can be reverted after deploy with no further considerations

The DB migrations should be rolled back if the code is removed permanently.

Labels & Review

  • Risk label is set correctly
  • The set of people pinged as reviewers is appropriate for the level of risk of the change

@snopoke snopoke added the product/invisible Change has no end-user visible impact label Apr 23, 2024
@snopoke snopoke requested review from calellowitz and esoergel April 23, 2024 13:32
@dimagimon dimagimon added the reindex/migration Reindex or migration will be required during or before deploy label Apr 23, 2024
Comment thread corehq/apps/users/util.py
return ret


def django_user_from_couch_id(id):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unused and also broken since couch user docs don't contain a django_user field

'djangoUserId': user.pk,
'superUser': user.is_superuser,
'authToken': None,
'authToken': session_id,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

always return the session_id - this just makes it easier for formplayer. Since the session_id is passed as input there's no issue including it in the output.

@dannyroberts dannyroberts left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks incredible

from corehq.util.log import send_HTML_email


@periodic_task(run_every=crontab(minute=0, hour=0))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean once a day?

@esoergel esoergel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't made it through the whole thing yet

Comment thread corehq/apps/app_execution/api.py Outdated
def _get_screen_and_data(self, current_data):
if not current_data:
return ScreenType.START, None
data = current_data.get("commands")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to apply a schema or encode some expectations about the structure of current_data?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use dataclasses or attrs for each of the response types

return factory.form_response(selections, self.children)


APP1 = Menu(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread corehq/apps/app_execution/discovery.py Outdated
yield exploration
continue

exploration.execute_next_step()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's with this stepwise execution? Looks like it's to go breadth-first rather than depth first, but it's evaluated fully either way, right? Just curious what that provides versus a recursive traversal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ha, just saw commit # 2

def execute_workflow(session: FormplayerSession, workflow):
with session:
session.sync()
execute_step(session, None)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would you think of moving self.get_session_start_data() here in place of None so that execute_step can assume step is always specified?

Comment thread corehq/apps/ota/decorators.py Outdated
Endpoints with this decorator will not enforce two factor authentication.
"""
return get_multi_auth_decorator(default=BASIC, oauth_scopes=['sync'])(
return get_multi_auth_decorator(default=BASIC, allow_formplayer=True, oauth_scopes=['sync'])(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you tell me more about this change? why did mobile_auth not previously allow formplayer auth? Was it that only smsforms needed it previously, and that doesn't need these endpoints?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this and pushed it to #34441 in a different way: f16faf3

But to answer your question, there are some endpoints where we only expect mobile to auth and not formplayer so it makes sense to keep those separate. For endpoints that both access we can use the mobile_auth_or_formplayer decorator.

Comment thread corehq/util/jsonattrs.py


@define
class AttrsObjectBuilder:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the case that an AttrsList is a list of objects, and an AttrsDict is a dict whose values are objects, whereas this represents a single object? There are some repeated patterns between the three - would be nice to combine the builders reflecting the commonalities if that's doable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it the case that an AttrsList is a list of objects, and an AttrsDict is a dict whose values are objects, whereas this represents a single object?

Yes, that's correct.

There are some repeated patterns between the three - would be nice to combine the builders reflecting the commonalities if that's doable.

I'll take a look

def execute_step(self, step):
data = self.get_request_data(step) if step else self.get_session_start_data()
self.data = self.client.make_request(data, self.request_url(step))
self.log_step(step)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is nice

user_id = models.CharField(max_length=36)
django_user = models.ForeignKey(User, on_delete=models.CASCADE)
workflow = AttrsObject(AppWorkflow)
form_mode = models.CharField(max_length=255, choices=FORM_MODE_CHOICES)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW models.TextChoices is a nice pattern for this sort of thing

Comment thread corehq/apps/app_execution/forms.py Outdated
def final_clean_user_id(self):
domain = self.cleaned_data.get("domain")
try:
CommCareUser.get_by_user_id(domain, self.cleaned_data.get("user_id"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it have to be a CommCareUser? USH is working to have web users submitting data too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have to be but for this use case I thought it made more sense for it to be a mobile user since they are easier to manage.


class AppWorkflowManager(models.Manager):
def get_due(self):
cutoff = functions.Now() - MakeInterval("mins", models.F("run_every"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fancy

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done this a few times now in different projects: https://www.snopoke.com/2023/03/11/django-intervals/

@esoergel esoergel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finished. There are some parts I could've gone more in depth reviewing, so please let me know if there's anything in particular you want a more detailed review

build_on = "Latest Version"
if app.built_on:
build_on = app.built_on.strftime("%B %d, %Y")
print(f"Using app '{app.name}' ({app._id} - {build_on})", file=self.log)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be useful to log the app version number too - easier to cross reference on the releases page.

@AmitPhulera AmitPhulera merged commit 5e46ccf into master Apr 24, 2024
@AmitPhulera

Copy link
Copy Markdown
Contributor

@esoergel Not sure if you have some blocking comments, maybe @snopoke can address them in a separate PR.
I have merged it for now to ensure that we have it up for testing today in EST. It felt safe to merge as it was a standalone thing and should not affect any part of HQ.

@snopoke snopoke deleted the sk/app-execution branch April 24, 2024 07:38
@sentry

sentry Bot commented Apr 30, 2024

Copy link
Copy Markdown
Contributor

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ KeyError: 'field' /hq/admin/workflows/new/ View Issue
  • ‼️ FormplayerException: Screen QueryScreen[ ] handling input ae76030e02e74264bc680f814ab86cc0 threw exception Index 1 ou... /hq/admin/workflows/test/{pk} View Issue

Did you find this useful? React with a 👍 or 👎

@snopoke snopoke mentioned this pull request Jun 11, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

product/invisible Change has no end-user visible impact reindex/migration Reindex or migration will be required during or before deploy

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants