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

feat(apple): Add support for iOS #334

Merged
merged 26 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
fedfcc2
iOS Support
brustolin Jul 5, 2023
460207b
Merge branch 'master' into feat/apple-support
brustolin Jul 5, 2023
30244d0
Delete launch.json
brustolin Jul 5, 2023
2510c4f
Update CHANGELOG.md
brustolin Jul 5, 2023
b260076
Update bin.ts
brustolin Jul 5, 2023
09c22cf
Update code-tools.ts
brustolin Jul 5, 2023
67c2e9f
Update ChooseIntegration.ts
brustolin Jul 5, 2023
1a1c17e
Apply suggestions from code review
brustolin Jul 6, 2023
c7acc22
some fixes
brustolin Jul 6, 2023
6aca98c
Merge branch 'feat/apple-support' of https://github.com/getsentry/sen…
brustolin Jul 6, 2023
af68bc3
Update apple-wizard.ts
brustolin Jul 6, 2023
819317a
Merge branch 'master' into feat/apple-support
brustolin Jul 6, 2023
db2c85d
Update Constants.ts
brustolin Jul 6, 2023
8b693e5
Merge branch 'feat/apple-support' of https://github.com/getsentry/sen…
brustolin Jul 6, 2023
f0303d5
Apply suggestions from code review
brustolin Jul 6, 2023
5ed26db
Merge branch 'master' into feat/apple-support
brustolin Jul 7, 2023
24d6664
multiple projects in the same folder
brustolin Jul 7, 2023
12aefe2
Merge branch 'feat/apple-support' of https://github.com/getsentry/sen…
brustolin Jul 7, 2023
1c754f0
Update CHANGELOG.md
brustolin Jul 7, 2023
ae9bac2
Update xcode-manager.ts
brustolin Jul 7, 2023
f03f50a
Update xcode-manager.ts
brustolin Jul 7, 2023
c9ca7c8
telemetry
brustolin Jul 10, 2023
b70a26e
Update Wizard.ts
brustolin Jul 10, 2023
44b8bde
Update Wizard.ts
brustolin Jul 10, 2023
792e6aa
Apply suggestions from code review
brustolin Jul 10, 2023
efbdd1c
Merge branch 'master' into feat/apple-support
brustolin Jul 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- feat: Add support for iOS (#334)
- feat: Open browser when logging in (sourcemaps, sveltekit, nextjs) (#328)
- fix: Support `--url` arg in NextJs, SvelteKit and Sourcemaps wizards (#331)
- fix: Update minimum Node version to Node 14 (#332)
Expand Down
5 changes: 5 additions & 0 deletions bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { run } from './lib/Setup';
import { runNextjsWizard } from './src/nextjs/nextjs-wizard';
import { runSourcemapsWizard } from './src/sourcemaps/sourcemaps-wizard';
import { runSvelteKitWizard } from './src/sveltekit/sveltekit-wizard';
import { runAppleWizard } from './src/apple/apple-wizard';
import { withTelemetry } from './src/telemetry';
import { WizardOptions } from './src/utils/types';
export * from './lib/Setup';
Expand Down Expand Up @@ -90,6 +91,10 @@ switch (argv.i) {
// eslint-disable-next-line no-console
).catch(console.error);
break;
case 'ios':
// eslint-disable-next-line no-console
runAppleWizard(wizardOptions).catch(console.error);
break
default:
runOldWizard();
}
Expand Down
5 changes: 5 additions & 0 deletions lib/Constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum Integration {
nextjs = 'nextjs',
sveltekit = 'sveltekit',
sourcemaps = 'sourcemaps',
ios = 'ios',
}

/** Key value should be the same here */
Expand Down Expand Up @@ -45,6 +46,8 @@ export function getIntegrationDescription(type: string): string {
return 'SvelteKit';
case Integration.sourcemaps:
return 'Configure Source Maps Upload';
case Integration.ios:
return 'iOS';
default:
return 'React Native';
}
Expand All @@ -64,6 +67,8 @@ export function mapIntegrationToPlatform(type: string): string | undefined {
return 'javascript-sveltekit';
case Integration.sourcemaps:
return undefined;
case Integration.ios:
return 'iOS';
default:
throw new Error(`Unknown integration ${type}`);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/Steps/ChooseIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Electron } from './Integrations/Electron';
import { NextJsShim } from './Integrations/NextJsShim';
import { ReactNative } from './Integrations/ReactNative';
import { SourceMapsShim } from './Integrations/SourceMapsShim';
import { Apple } from './Integrations/Apple';
import { SvelteKitShim } from './Integrations/SvelteKitShim';

let projectPackage: any = {};
Expand Down Expand Up @@ -48,6 +49,9 @@ export class ChooseIntegration extends BaseStep {
case Integration.sourcemaps:
integration = new SourceMapsShim(this._argv);
break;
case Integration.ios:
integration = new Apple(this._argv);
break;
default:
integration = new ReactNative(this._argv);
break;
Expand Down
19 changes: 19 additions & 0 deletions lib/Steps/Integrations/Apple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Answers } from 'inquirer';
import type { Args } from '../../Constants';
import { BaseIntegration } from './BaseIntegration';
import { runAppleWizard } from '../../../src/apple/apple-wizard';

export class Apple extends BaseIntegration {
public constructor(protected _argv: Args) {
super(_argv);
}

public async emit(_answers: Answers): Promise<Answers> {
await runAppleWizard({ promoCode: this._argv.promoCode, url: this._argv.url, });
return {};
}

public async shouldConfigure(_answers: Answers): Promise<Answers> {
return this._shouldConfigure;
}
}
94 changes: 94 additions & 0 deletions src/apple/apple-wizard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/* eslint-disable max-lines */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unused-vars */
// @ts-ignore - clack is ESM and TS complains about that. It works though
import clack from '@clack/prompts';
import chalk from 'chalk';
import * as fs from 'fs';
import * as path from 'path';
import * as templates from './templates';
import * as xcManager from './xcode-manager';
import * as codeTools from './code-tools';
import * as bash from '../utils/bash';
import { WizardOptions } from '../utils/types';

const xcode = require('xcode');
/* eslint-enable @typescript-eslint/no-unused-vars */

import {
askForProjectSelection,
askForSelfHosted,
askForWizardLogin,
askToInstallSentryCLI,
SentryProjectData,
printWelcome,
abort,
} from '../utils/clack-utils';

export async function runAppleWizard(
options: WizardOptions,
): Promise<void> {
printWelcome({
wizardName: 'Sentry Apple Wizard',
promoCode: options.promoCode,
});


if (!bash.hasSentryCLI()) {
if (!(await askToInstallSentryCLI())) {
clack.log.warn("Without Sentry-cli, you won't be able to upload debug symbols to Sentry. You can install it later by following the instructions at https://docs.sentry.io/cli/");
brustolin marked this conversation as resolved.
Show resolved Hide resolved
} else {
await bash.installSentryCLI();
}
}

const projectDir = process.cwd();
const xcodeProjFile = findFilesWithExtension(projectDir, ".xcodeproj")[0];

if (!xcodeProjFile) {
clack.log.error('No xcode project found. Please run this command from the root of your project.');
brustolin marked this conversation as resolved.
Show resolved Hide resolved
brustolin marked this conversation as resolved.
Show resolved Hide resolved
await abort();
return;
brustolin marked this conversation as resolved.
Show resolved Hide resolved
}

const pbxproj = path.join(projectDir, xcodeProjFile, "project.pbxproj");
Copy link
Member

Choose a reason for hiding this comment

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

Just to sanity-check (for someone who has no idea about xcode/ios apps): Is this a valid path?

wizard/wizard-apple/test.xcodeproj/project.pbxproj

Background: I briefly tried the wizard locally but couldn't get beyond the pbxproj step:

image

Copy link
Contributor Author

@brustolin brustolin Jul 6, 2023

Choose a reason for hiding this comment

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

Yes. The ".xcodeproj" is actually a folder that keeps project configuration files.

I think we should not show the "Successfully..." message in this case.

I briefly tried the wizard locally but couldn't get beyond the pbxproj step:

You just tried to run it, or you really have an xcode project to test it? Because it should have worked if you have a project.

Copy link
Member

Choose a reason for hiding this comment

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

Yes. The ".xcodeproj" is actually a folder that keeps project configuration files.

Okay, all good then! I guess I was just confused by the xcodeProjectFile variable then 😅

You just tried to run it

Yeah, but not very methodically and without an actual xcode project. Just wanted to get over a few steps so I created some empty files 😅

Copy link
Member

Choose a reason for hiding this comment

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

@brustolin do you know a repo which would be a example app to test this stuff with ourselves?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

brustolin marked this conversation as resolved.
Show resolved Hide resolved
if (!fs.existsSync(pbxproj)) {
clack.log.error(`No pbxproj found at ${pbxproj}`);
await abort();
return;
}

const { project, apiKey } = await getSentryProjectAndApiKey(options.promoCode, options.url);

xcManager.updateXcodeProject(pbxproj, project, apiKey, true, true);

const codeAdded = codeTools.addCodeSnippetToProject(projectDir, project.keys[0].dsn.public);
if (!codeAdded) {
clack.log.warn('Sentry dependency was added to your project, but could not add Sentry code snippet to it. Please add it manually by following this: https://docs.sentry.io/platforms/apple/guides/ios/#configure');
brustolin marked this conversation as resolved.
Show resolved Hide resolved
return;
}

clack.log.success('Sentry was successfully added to your project!');
}

//Prompt for Sentry project and API key
async function getSentryProjectAndApiKey(promoCode?: string, url?: string): Promise<{ project: SentryProjectData, apiKey: { token: string } }> {
const { url: sentryUrl } = await askForSelfHosted(url);

const { projects, apiKeys } = await askForWizardLogin({
promoCode: promoCode,
url: sentryUrl,
platform: 'javascript-nextjs',
brustolin marked this conversation as resolved.
Show resolved Hide resolved
});

const selectedProject = await askForProjectSelection(projects);
return { project: selectedProject, apiKey: apiKeys };
}

//find files with the given extension
function findFilesWithExtension(dir: string, extension: string): string[] {
const files = fs.readdirSync(dir);
return files.filter(file => file.endsWith(extension));
}
77 changes: 77 additions & 0 deletions src/apple/code-tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as fs from 'fs';
import * as path from 'path';
import * as templates from './templates';

const swiftAppLaunchRegex = /(func\s+application\s*\(_\sapplication:[^,]+,\s*didFinishLaunchingWithOptions[^,]+:[^)]+\)\s+->\s+Bool\s+{)|(init\s*\([^)]*\)\s*{)/gim;
const objcAppLaunchRegex = /-\s*\(BOOL\)\s*application:\s*\(UIApplication\s*\*\)\s*application\s+didFinishLaunchingWithOptions:\s*\(NSDictionary\s*\*\)\s*launchOptions\s*{/gim;


function isAppDelegateFile(filePath: string): boolean {
const appLaunchRegex = filePath.toLowerCase().endsWith(".swift") ? swiftAppLaunchRegex : objcAppLaunchRegex;

const fileContent = fs.readFileSync(filePath, 'utf8');
return (fileContent.includes("UIApplicationDelegate") && appLaunchRegex.test(fileContent)) || /struct[^:]+:\s*App\s*{/.test(fileContent);
brustolin marked this conversation as resolved.
Show resolved Hide resolved
}

function findAppDidFinishLaunchingWithOptions(dir: string): string | null {
const files = fs.readdirSync(dir);
//iterate over subdirectories later,
//the appdelegate usually is in the top level
const dirs: string[] = [];

for (const file of files) {
const filePath = path.join(dir, file);
if (file.endsWith(".swift") || file.endsWith(".m") || file.endsWith(".mm")) {
if (isAppDelegateFile(filePath)) {
return filePath;
}
} else if (!file.startsWith(".") && !file.endsWith(".xcodeproj") && !file.endsWith(".xcassets") && fs.lstatSync(filePath).isDirectory()) {
dirs.push(file);
}
}

for (const dr of dirs) {
const result = findAppDidFinishLaunchingWithOptions(path.join(dir, dr));
if (result) return result;
}
return null;
}

export function addCodeSnippetToProject(projPath: string, dsn: string): boolean {
const appDelegate = findAppDidFinishLaunchingWithOptions(projPath);
if (!appDelegate) {
return false;
}

const fileContent = fs.readFileSync(appDelegate, 'utf8');
const isSwift = appDelegate.toLowerCase().endsWith(".swift");
const appLaunchRegex = isSwift ? swiftAppLaunchRegex : objcAppLaunchRegex;
const importStatement = isSwift ? "import Sentry\n" : "@import Sentry;\n";
const checkForSentryInit = isSwift ? "SentrySDK.start" : "[SentrySDK start";
let codeSnippet = isSwift ? templates.getSwiftSnippet(dsn) : templates.getObjcSnippet(dsn);

if (fileContent.includes(checkForSentryInit)) {
//already initialized
return true;
}

let match = appLaunchRegex.exec(fileContent);
if (!match) {
const swiftUIMatch = /struct[^:]+:\s*App\s*{/.exec(fileContent)
if (!swiftUIMatch) {
return false;
}
//Is SwiftUI with no init
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
match = swiftUIMatch;
codeSnippet = ` init() {\n${codeSnippet} }`;
}

const insertIndex = match.index + match[0].length;
const newFileContent = (fileContent.indexOf(importStatement) >= 0 ? "" : importStatement) +
fileContent.slice(0, insertIndex) + "\n" +
codeSnippet +
fileContent.slice(insertIndex);
fs.writeFileSync(appDelegate, newFileContent, 'utf8');

return true;
}
47 changes: 47 additions & 0 deletions src/apple/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export function getRunScriptTemplate(
orgSlug: string,
projectSlug: string,
apiKey: string,
uploadSource = true,
): string {
// eslint-disable-next-line no-useless-escape
return `# This script is responsable to upload debug symbols and source context for Sentry.\\nif which sentry-cli >/dev/null; then\\nexport SENTRY_ORG=${orgSlug}\\nexport SENTRY_PROJECT=${projectSlug}\\nexport SENTRY_AUTH_TOKEN=${apiKey}\\nERROR=$(sentry-cli debug-files upload ${uploadSource ? "--include-sources " : ""}\"$DWARF_DSYM_FOLDER_PATH\" 2>&1 >/dev/null)\\nif [ ! $? -eq 0 ]; then\\necho \"warning: sentry-cli - $ERROR\"\\nfi\\nelse\\necho \"warning: sentry-cli not installed, download from https://github.com/getsentry/sentry-cli/releases\"\\nfi\\n`;
}

export const scriptInputPath = "\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}\"";

export function getSwiftSnippet(dsn: string): string {
return ` SentrySDK.start { options in
options.dsn = "${dsn}"
options.enableTracing = true
brustolin marked this conversation as resolved.
Show resolved Hide resolved
#if DEBUG
options.debug = true
options.environment = "Development"
#else
options.environment = "Release"
#endif
brustolin marked this conversation as resolved.
Show resolved Hide resolved
options.attachScreenshot = true
options.attachViewHierarchy = true
Copy link
Member

Choose a reason for hiding this comment

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

h: I don't think we should enable that by default. Especially the screenshots could contain sensitive data, even in a development build.

Copy link
Member

Choose a reason for hiding this comment

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

we could also consider having them //commented out so it is easy enough for the user to decide themselves

Copy link
Member

@romtsn romtsn Jul 7, 2023

Choose a reason for hiding this comment

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

if I may, we could just add a comment to the snippet explaining that it can have sensitive data and turn it off in case you don't want it. Or we could add another step "Enable recommended features (could send PII to Sentry)"? and only after accepting it add the snippet

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 like the idea to show this options in the wizard and explain whats happening, but in another PR.

Copy link
Member

@philipphofmann philipphofmann Jul 10, 2023

Choose a reason for hiding this comment

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

I think adding a comment won't be sufficient. With the wizard, it can happen that some people don't check the code that much. They just hit compile and run after executing the wizard. We could add them //commented out, or an extra step would also be OK.

options.enableTimeToFullDisplay = true
philipphofmann marked this conversation as resolved.
Show resolved Hide resolved
}
//Remove the next line after running the app once.
SentrySDK.capture(message: "This app uses Sentry! :)")\n`;
}

export function getObjcSnippet(dsn: string): string {
return ` [SentrySDK startWithConfigureOptions:^(SentryOptions * options) {
options.dsn = @"${dsn}";
options.enableTracing = YES;
#if DEBUG
options.debug = YES;
options.environment = @"Development";
#else
options.environment = @"Release";
#endif
options.attachScreenshot = YES;
options.attachViewHierarchy = YES;
options.enableTimeToFullDisplay = YES;
}];
//Remove the next line after running the app once.
[SentrySDK captureMessage:@"This app uses Sentry!"]\n`;
}
Loading