Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Geocoding on activist update #197

Merged
merged 12 commits into from
Jun 25, 2023
Merged

Geocoding on activist update #197

merged 12 commits into from
Jun 25, 2023

Conversation

jefftienchow
Copy link
Contributor

@jefftienchow jefftienchow commented May 2, 2023

Attempt at geocoding addresses with google API on activist update

Tested locally on a variety of addresses. Google Geocoding API has a bit undefined behavior when entering bogus addresses but seems to work well with real addresses.

Not sure if I should include package-lock.json changes in my commits?

@jefftienchow jefftienchow changed the title Geocoding Geocoding on activist update May 2, 2023
@jakehobbs
Copy link
Member

Thanks for this! I will review later today.

@alexsapps
Copy link
Collaborator

alexsapps commented May 2, 2023 via email

@alexsapps
Copy link
Collaborator

Not sure if I should include package-lock.json changes in my commits?

Yes, maintaining package-lock.json changes in version control ensures reproducible builds. package.json only specifies a range of versions allowed to build the project, not exact versions.

@@ -1131,12 +1146,14 @@ func UpdateActivistData(db *sqlx.DB, activist ActivistExtra, userEmail string) (
// TODO: make this work for other chapters too, maybe based on Chapters table mailing list info?
// or maybe just passing chapter Name or ID to the signup service?
if origActivist.ChapterID == 47 {
activistInfoChanged := activist.Name != origActivist.Name ||
activistAddressChanged := activist.StreetAddress != origActivist.StreetAddress ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

looks like StreetAddress is not used by the mailing list, so this should not trigger a call to update the mailing list.

activist.City != origActivist.City ||
activist.State != origActivist.State
activistInfoChanged := activistAddressChanged ||
activist.Location != origActivist.Location ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

should location be considered in activistAddressChanged instead? it look like it contains the Zip which is used by the mailing list.

but then Zip is not used by the geo API, so maybe this logic should not be shared between geo api logic and mailing list logic, in which case using names like "isMailingListInfoChanged" and "isGeoInfoChanged" would better convey the intent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I don't think zip is used for geocoding so I'll do the latter

@@ -1155,7 +1172,23 @@ func UpdateActivistData(db *sqlx.DB, activist ActivistExtra, userEmail string) (
fmt.Println("ERROR updating activist on mailing list:", err.Error())
}
}
fmt.Println("Updated: ", activist.Location, activist.City, activist.State)
if activistAddressChanged && activist.StreetAddress != "" && activist.City != "" && activist.State != "" {
Copy link
Collaborator

Choose a reason for hiding this comment

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

please split the body of this 'if' into a separate function (e.g. called GetCoordinatesFromAddress) in a separate file (single responsibility principle). i suggest the function take a single string parameter for the address and return the location and any error.

additionally, but optionally, the 'if' itself could be its own function called updateActivistCoordinates which would allow early returns in case of error rather than nesting the happy path inside of an if block.

request := "https://maps.googleapis.com/maps/api/geocode/json?address=" + full_address + "&key=" + config.GooglePlacesAPIKey
resp, err := http.Get(request)
if err != nil {
fmt.Println("Error geocoding activist location", err)
Copy link
Collaborator

Choose a reason for hiding this comment

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

if there is an error might be better to break out of this if somehow (or return early if you split this into another function)

Copy link
Member

Choose a reason for hiding this comment

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

On this note, I think we'd probably prefer to silently fail (& log it) but still go through w/ saving the activist in the db if the gecocoding service fails for w/e reason.

I feel it's pretty important that activist data is still able to be updated even if the geocoding service is failing.

Copy link
Member

Choose a reason for hiding this comment

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

Thinking about this further... maybe it would be ideal to just fire off a goroutine here that handles the things that need to happen in the background, such as updating mailing list and coords? B/c we don't really want the user to have to wait around for those things to happen before the activist is saved in the db. (This was a bit of a flaw in the original logic for updating the mailing list.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

probably the coords should still be looked synchronously because the result is used in the database update? if so, could the signup be made async in a separate PR so Jeff's change isn't held up?

just wondering, is the async idea to make it faster when an admin of ADB clicks save to update a user? did you notice the adb signup was slow?

maybe whatever is simplest/easiest is best for this probably rarely used feature. but some possible improvements mainly as thought experiments:

  • it would be nice to know if the signup operations fails, maybe by an email notification to our team, perhaps directly or by monitoring error logs.
  • it would be nice to ensure async operations complete even if the web server container is requested to shut down or even if an error in the main function causes it to return and shut down. perhaps there would be a work queue that the signup gets added to, and the main function can wait for the queue on the last line before exiting.

Copy link
Member

Choose a reason for hiding this comment

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

Fair points. Yeah, I was thinking the main reason would be so it saves faster, but I that might be a bit of an over optimization for now. I def won't block on it.

We have log drains in AWS and can set up Cloudwatch alerts for whatever.

The signup service has its own queue. That being said, I do think the background jobs that run in ADB could use a huge overhaul at some point if we have bandwidth. Right now they only run within a single instance of the app (via an environment variable that enables them). It would be great to implement a more robust job system.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe walking through how the signup service works at the next meeting would be useful. A lot of stuff feeds in and out of it, and would be good for others to know how it works.

@@ -441,6 +444,18 @@ type GetActivistOptions struct {
UpcomingFollowupsOnly bool `json:"upcoming_followups_only"`
}

type GeocodeResponse struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

please a comment indicating that this structure corresponds to Google's geo coding API with a link to that API's documentation

Copy link
Member

@jakehobbs jakehobbs left a comment

Choose a reason for hiding this comment

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

I took a brief look over this and p much agree w/ what Alex pointed out. Left one comment on one of his comments, but I don't have anything else to add other than that.

Copy link
Collaborator

@alexsapps alexsapps left a comment

Choose a reason for hiding this comment

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

Thanks for the updates! Just got a few more suggestions.

geoInfoChanged := activist.City != origActivist.City ||
activist.State != origActivist.State ||
activist.StreetAddress != origActivist.StreetAddress
mailingListInfoChanged := activist.Name != origActivist.Name ||
activist.Email != origActivist.Email ||
activist.Phone != origActivist.Phone ||
activist.Location != origActivist.Location ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

would be nice to move this down a couple lines underneath the state != state check to keep things in the same order that they are used later on, to more easily verify correctness at a glance.

@@ -1131,14 +1145,17 @@ func UpdateActivistData(db *sqlx.DB, activist ActivistExtra, userEmail string) (
// TODO: make this work for other chapters too, maybe based on Chapters table mailing list info?
// or maybe just passing chapter Name or ID to the signup service?
if origActivist.ChapterID == 47 {
activistInfoChanged := activist.Name != origActivist.Name ||
geoInfoChanged := activist.City != origActivist.City ||
Copy link
Collaborator

Choose a reason for hiding this comment

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

please declare this where it is used, just above if geoInfoChanged && ...

fmt.Printf("No geocoding results found for address %v. Not updating Lat and Lng", full_address)
return nil
} else {
return &Location{Lat: geocode_response.Results[0].Geometry.Location.Lat, Lng: geocode_response.Results[0].Geometry.Location.Lng}
Copy link
Collaborator

Choose a reason for hiding this comment

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

If you wouldn't mind checking if Results[0] and Geometry and Location are non-nil, I think that could prevent a segfault. Even if the API always returns non-nil now, I worry this could change and we wouldn't want the server to crash - just log an error and carry on.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm a beginner at Go, but from my understanding this wouldn't be possible? Constructing a GeocodeResponse from json that doesn't contain these fields would just result in the default values being included for lat and lng, which would be 0 and 0

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh got it. My bad :) I'm a Go beginner too. So a struct inside struct has a default value of the 0 value of the inner struct, as opposed to a struct pointer inside a struct which would be nil.

Copy link
Collaborator

@alexsapps alexsapps left a comment

Choose a reason for hiding this comment

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

Looks good to me. Let's see if Jake has any suggestions since it's the first PR in this codebase for either of us, I think.

Can you select "squash" merge strategy rather than "merge"? Otherwise it will clutter the main branch log since the many commits in this branch are all for one logical change. If it lets you type a commit message, perhaps use something slightly more precise like "Geocode activist address on update"

resp, err := http.Get(request)
if err != nil {
fmt.Println("Error geocoding activist location", err)
return nil
Copy link
Collaborator

Choose a reason for hiding this comment

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

optional: return the error (and print it in the caller) to allow callers to handle errors as needed, thereby making this function more reusable

Copy link
Member

Choose a reason for hiding this comment

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

yeah i agree returning the error would be good as best practice, then the caller could just choose to ignore it if you don't want to actually handle it.

fmt.Println("Error geocoding activist location", err)
return nil
}
defer resp.Body.Close()
Copy link
Collaborator

Choose a reason for hiding this comment

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

consider adding a blank line between the HTTP fetching logic and GeocodeResponse parsing logic.

fmt.Printf("No geocoding results found for address %v. Not updating Lat and Lng", full_address)
return nil
} else {
return &Location{Lat: geocode_response.Results[0].Geometry.Location.Lat, Lng: geocode_response.Results[0].Geometry.Location.Lng}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh got it. My bad :) I'm a Go beginner too. So a struct inside struct has a default value of the 0 value of the inner struct, as opposed to a struct pointer inside a struct which would be nil.

resp, err := http.Get(request)
if err != nil {
fmt.Println("Error geocoding activist location", err)
return nil
Copy link
Member

Choose a reason for hiding this comment

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

yeah i agree returning the error would be good as best practice, then the caller could just choose to ignore it if you don't want to actually handle it.

@jefftienchow jefftienchow merged commit 07a0717 into master Jun 25, 2023
@jefftienchow jefftienchow deleted the geocoding branch June 25, 2023 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants